proof_engine/lib.rs
1#![allow(dead_code, unused_variables, unused_imports, unused_mut, unused_parens, non_snake_case, unreachable_patterns, unused_assignments, unused_labels, unused_doc_comments, private_interfaces, non_camel_case_types, non_upper_case_globals, clippy::all)]
2
3//! # Proof Engine
4//!
5//! A Rust engine for real-time graphics where every point, glyph and
6//! particle on screen is moved by a real equation: an ODE, a force field, a
7//! spring, a noise function.
8//!
9//! <img src="https://gitlab.com/mattbusel/proof-engine/-/raw/main/assets/gifs/strange_attractors.gif" width="100%" alt="Seven strange attractors, each 1,500 points integrated with RK4">
10//!
11//! ## The math runs without a window
12//!
13//! The integrators, attractors, fields and [`MathFunction`]s are plain Rust
14//! and can be used on their own. This example runs as a doc test:
15//!
16//! ```rust
17//! use proof_engine::math::attractors::{rk4_step, AttractorType};
18//! use proof_engine::prelude::Vec3;
19//!
20//! // Two Lorenz states that start 0.0001 apart...
21//! let mut a = Vec3::new(1.0, 1.0, 1.0);
22//! let mut b = Vec3::new(1.0001, 1.0, 1.0);
23//! for _ in 0..6000 {
24//! a = rk4_step(AttractorType::Lorenz, a, 0.005);
25//! b = rk4_step(AttractorType::Lorenz, b, 0.005);
26//! }
27//! // ...are far apart after 30 simulated seconds: that is chaos.
28//! assert!((a - b).length() > 1.0);
29//! ```
30//!
31//! ## Open a window and draw
32//!
33//! The `quickstart` example, in full. It needs a GPU with OpenGL 3.3, so it
34//! is compiled here but not run:
35//!
36//! ```rust,no_run
37//! use proof_engine::math::attractors::rk4_step;
38//! use proof_engine::prelude::*;
39//! use proof_engine::render::ui_layer::UiParticle;
40//!
41//! let mut engine = ProofEngine::new(EngineConfig::default());
42//! let mut points: Vec<Vec3> = (0..5000)
43//! .map(|i| Vec3::new(1.0 + i as f32 * 4e-4, 1.0, 1.0))
44//! .collect();
45//!
46//! engine.run_ui(move |engine, dt| {
47//! for p in points.iter_mut() {
48//! *p = rk4_step(AttractorType::Lorenz, *p, dt);
49//! }
50//! let (w, h) = engine.render_size();
51//! let (cx, cy, s) = (w as f32 / 2.0, h as f32 / 2.0, h as f32 / 60.0);
52//! let color = Vec4::new(0.5, 1.2, 1.6, 1.0);
53//! let dots = points
54//! .iter()
55//! .map(|p| UiParticle::new(cx + p.x * s, cy - (p.z - 25.0) * s, 3.0, 3.0, '●', color))
56//! .collect();
57//! engine.ui.draw_particles(dots);
58//! });
59//! ```
60//!
61//! ## Where to look
62//!
63//! - [`ProofEngine`]: the window and main loop. [`ProofEngine::run`] for 3D
64//! glyph scenes, [`ProofEngine::run_ui`] for screen-space games and plots.
65//! - [`EngineConfig`] and [`config::RenderConfig`]: window size, bloom,
66//! tonemap, grain and every other post-processing setting.
67//! - [`Glyph`] and [`ProofEngine::spawn_glyph`]: the basic on-screen object.
68//! - [`ForceField`], [`MathFunction`] and [`math::attractors`]: the math.
69//! - [`render::ui_layer::UiParticle`]: fast screen-space points, as above.
70//! - [`prelude`]: the common imports in one line.
71//!
72//! Every example in the repository can save its own frames to disk with the
73//! `PROOF_SHOT` environment variables; see the
74//! [README](https://gitlab.com/mattbusel/proof-engine#capture-frames-from-any-program).
75
76pub mod math;
77pub mod glyph;
78pub mod entity;
79pub mod particle;
80pub mod scene;
81pub mod render;
82pub mod audio;
83pub mod integration;
84pub mod input;
85pub mod config;
86pub mod tween;
87pub mod debug;
88pub mod ui;
89pub mod timeline;
90pub mod procedural;
91pub mod physics;
92pub mod combat;
93pub mod spatial;
94pub mod effects;
95pub mod anim;
96pub mod animation;
97pub mod ai;
98pub mod networking;
99pub mod replay;
100pub mod scripting;
101pub mod terrain;
102pub mod ecs;
103pub mod editor;
104pub mod asset;
105pub mod save;
106/// Character stats, inventory, skills and quests.
107pub mod character;
108pub mod dsp;
109pub mod game;
110pub mod profiler;
111pub mod vfx;
112pub mod netcode;
113pub mod network;
114pub mod world;
115pub mod crafting;
116/// Navigation meshes, A* search and steering.
117pub mod pathfinding;
118pub mod economy;
119pub mod behavior;
120/// Atmosphere, precipitation and climate simulation.
121pub mod weather;
122pub mod deferred;
123pub mod shader_graph;
124pub mod surfaces;
125pub mod rendergraph;
126pub mod compute;
127pub mod lighting;
128pub mod number_theory;
129/// Graph data structures, layout, community detection, paths and flows.
130pub mod graph;
131/// Hyperbolic, spherical and toroidal spaces and portals.
132pub mod topology;
133pub mod stochastic;
134pub mod ml;
135pub mod wgpu_backend;
136pub mod geometry;
137pub mod symbolic;
138pub mod solver;
139pub mod fractal;
140pub mod metaball;
141pub mod worldgen;
142pub mod ecology;
143pub mod narrative;
144pub mod electromagnetic;
145pub mod relativistic;
146/// Quantum mechanics simulations: Schrodinger solvers, tunneling, entanglement and more.
147pub mod quantum;
148/// Sparse voxel octree global illumination (voxel cone tracing), not wired into the demos.
149pub mod svogi;
150pub mod curves;
151pub mod nishita_sky;
152pub mod volumetric_fog;
153pub mod tiled_lighting;
154pub mod export;
155mod capture;
156
157pub use config::EngineConfig;
158pub use math::{MathFunction, ForceField, Falloff, AttractorType};
159pub use glyph::{Glyph, RenderLayer, BlendMode};
160pub use entity::AmorphousEntity;
161pub use particle::{MathParticle, ParticleInteraction};
162pub use scene::SceneGraph;
163pub use render::camera::ProofCamera;
164pub use input::{InputState, Key};
165pub use render::pipeline::FrameStats;
166pub use audio::AudioEvent;
167
168/// The main engine struct. Create once, run forever.
169pub struct ProofEngine {
170 /// Engine settings; most render settings are read every frame.
171 pub config: EngineConfig,
172 /// The glyphs, particles and force fields in the world.
173 pub scene: SceneGraph,
174 /// The camera the world pass is drawn from.
175 pub camera: ProofCamera,
176 /// Keyboard and mouse state for the current frame.
177 pub input: InputState,
178 /// Screen-space UI, in pixel coordinates. Cleared at the start of every
179 /// frame by `run_ui`, so games redraw it immediate-mode style.
180 pub ui: render::ui_layer::UiLayer,
181 /// Transient screen effects: shockwaves, flashes, light shafts. Fire and
182 /// forget; ticked and uploaded by `run_ui` every frame.
183 pub fx: render::screen_fx::ScreenFx,
184 /// GPU density entities queued for this frame. Drained after the render.
185 density_queue: Vec<particle::gpu_density::GpuDensityEntityData>,
186 /// The particle budget per density entity, set by `init_gpu_density`.
187 density_budget: u32,
188 /// Optional audio engine — None if no output device is available.
189 pub audio: Option<audio::AudioEngine>,
190 // Internal render pipeline (initialized lazily when run() is called)
191 pipeline: Option<render::Pipeline>,
192}
193
194impl ProofEngine {
195 /// Create the engine. The window and GL context are created when `run`
196 /// or `run_ui` is called; audio starts now if enabled and a device exists.
197 pub fn new(mut config: EngineConfig) -> Self {
198 if let Some((w, h)) = capture::window_size() {
199 config.window_width = w;
200 config.window_height = h;
201 }
202 let audio = if config.audio.enabled {
203 audio::AudioEngine::try_new()
204 } else {
205 None
206 };
207 Self {
208 camera: ProofCamera::new(&config),
209 scene: SceneGraph::new(),
210 input: InputState::new(),
211 ui: render::ui_layer::UiLayer::new(
212 config.window_width as f32,
213 config.window_height as f32,
214 ),
215 fx: render::screen_fx::ScreenFx::new(),
216 density_queue: Vec::new(),
217 density_budget: 0,
218 audio,
219 config,
220 pipeline: None,
221 }
222 }
223
224 /// Turn on GPU density entities with a per-entity particle budget.
225 ///
226 /// The budget is capped at
227 /// [`MAX_PARTICLES_PER_ENTITY`](particle::gpu_density::MAX_PARTICLES_PER_ENTITY):
228 /// past that there are more particles than pixels and the picture stops
229 /// improving while the frame time keeps climbing. Asking for more is
230 /// fine; you get the cap and a log line.
231 pub fn init_gpu_density(&mut self, particles: u32) {
232 let cap = particle::gpu_density::MAX_PARTICLES_PER_ENTITY;
233 if particles > cap {
234 log::info!("gpu density: {particles} particles requested, drawing {cap} per entity");
235 }
236 self.density_budget = particles.min(cap);
237 }
238
239 /// Draw a density entity this frame. Call every frame it should show.
240 pub fn queue_gpu_density_entity(&mut self, entity: particle::gpu_density::GpuDensityEntityData) {
241 self.density_queue.push(entity);
242 }
243
244 /// Send an audio event. No-op if audio is unavailable.
245 pub fn emit_audio(&self, event: audio::AudioEvent) {
246 if let Some(ref a) = self.audio {
247 a.emit(event);
248 }
249 }
250
251 /// Run the engine. Calls `update` every frame with elapsed seconds.
252 /// Blocks until the window is closed.
253 pub fn run<F>(&mut self, mut update: F)
254 where
255 F: FnMut(&mut ProofEngine, f32),
256 {
257 self.run_with_overlay(move |engine, dt, _gl| {
258 update(engine, dt);
259 });
260 }
261
262 /// Run the engine with an overlay callback.
263 /// The overlay callback receives the glow GL context reference and is called
264 /// AFTER scene rendering but BEFORE buffer swap — perfect for egui.
265 pub fn run_with_overlay<F>(&mut self, mut update: F)
266 where
267 F: FnMut(&mut ProofEngine, f32, &glow::Context),
268 {
269 let pipeline = render::Pipeline::init(&self.config);
270 self.pipeline = Some(pipeline);
271 let mut capture = capture::Capture::from_env();
272 let fixed_dt = capture::fixed_dt();
273
274 let mut last = std::time::Instant::now();
275 loop {
276 let now = std::time::Instant::now();
277 let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
278 last = now;
279
280 // Poll input
281 if let Some(ref mut p) = self.pipeline {
282 if !p.poll_events(&mut self.input) {
283 break;
284 }
285 }
286
287 // Step force fields and physics
288 self.scene.tick(dt);
289
290 // User update (logic only — no GL calls here)
291 // We pass a dummy gl ref for the logic phase; the real painting
292 // happens after the scene render.
293 let gl_ptr = self.pipeline.as_ref().map(|p| p.gl() as *const glow::Context);
294
295 // Sync render config so runtime changes (particle_multiplier, bloom, etc.)
296 // take effect this frame.
297 if let Some(ref mut p) = self.pipeline {
298 p.update_render_config(&self.config.render);
299 }
300
301 // Render scene first
302 if let Some(ref mut p) = self.pipeline {
303 p.set_density_entities(&self.density_queue, self.density_budget);
304 p.render(&self.scene, &self.camera);
305 }
306 self.density_queue.clear();
307 self.fx.lights.clear();
308
309 // NOW paint the overlay (egui) on top of the rendered scene
310 if let Some(ptr) = gl_ptr {
311 let gl_ref = unsafe { &*ptr };
312 update(self, dt, gl_ref);
313 }
314
315 // PROOF_SHOT: read the finished frame back before it is swapped away.
316 if let Some(ref mut c) = capture {
317 if c.after_draw(self) {
318 break;
319 }
320 }
321
322 // Honour a quit asked for during the update, as `run_ui` does.
323 if self.input.quit_requested {
324 break;
325 }
326
327 // Swap
328 if let Some(ref mut p) = self.pipeline {
329 if !p.swap() {
330 break;
331 }
332 }
333 }
334 }
335
336 /// Run a UI-driven game.
337 ///
338 /// Unlike \[`run`\], `update` is called *before* the scene is drawn, and the
339 /// screen-space `ui` layer is painted afterwards. That ordering matters for
340 /// a game: what you push this frame is what appears this frame, rather than
341 /// showing up one frame late.
342 ///
343 /// The UI layer is cleared before each `update`, so games redraw it in full
344 /// every frame instead of tracking what to erase.
345 pub fn run_ui<F>(&mut self, mut update: F)
346 where
347 F: FnMut(&mut ProofEngine, f32),
348 {
349 let pipeline = render::Pipeline::init(&self.config);
350 self.pipeline = Some(pipeline);
351
352 // Size the UI layer from the framebuffer, which is what the viewport
353 // uses; the window's own size can differ on a scaled display.
354 let (w, h) = self.render_size();
355 self.ui.resize(w as f32, h as f32);
356
357 let mut capture = capture::Capture::from_env();
358 let fixed_dt = capture::fixed_dt();
359
360 let mut last = std::time::Instant::now();
361 let mut last_size = (w, h);
362 loop {
363 let now = std::time::Instant::now();
364 let dt = fixed_dt.unwrap_or_else(|| now.duration_since(last).as_secs_f32().min(0.1));
365 last = now;
366
367 if let Some(ref mut p) = self.pipeline {
368 if !p.poll_events(&mut self.input) {
369 break;
370 }
371 }
372
373 // Keep the UI projection matched to the framebuffer.
374 let size = self.render_size();
375 if size != last_size {
376 last_size = size;
377 self.ui.resize(size.0 as f32, size.1 as f32);
378 }
379
380 self.scene.tick(dt);
381
382 // Game logic and UI construction, both before anything is drawn.
383 self.ui.begin_frame();
384 update(self, dt);
385
386 // Honour a quit asked for during the update.
387 //
388 // `request_quit` used to set a flag that nothing read, so a game's
389 // own Quit menu did nothing at all and the only way out was the
390 // window's close button. The check goes here, after the update and
391 // before the render, so the frame that asked to quit is the last
392 // one and nothing half-drawn reaches the screen.
393 if self.input.quit_requested {
394 break;
395 }
396
397 // Trauma decays here. It used to be added and never ticked in
398 // this loop, so the first hit left the camera shaking forever.
399 self.camera.shake.tick(dt);
400 self.fx.tick(dt);
401
402 if let Some(ref mut p) = self.pipeline {
403 p.update_render_config(&self.config.render);
404 p.set_density_entities(&self.density_queue, self.density_budget);
405 // The scene, then the UI's world pass into the same buffer,
406 // then post-processing over both.
407 p.render_frame(&self.scene, &self.camera, Some(&self.ui), &self.fx);
408 // The HUD, painted after post-processing so it stays sharp.
409 p.render_ui(&self.ui);
410 }
411 self.density_queue.clear();
412 self.fx.lights.clear();
413
414 if let Some(ref mut c) = capture {
415 if c.after_draw(self) {
416 break;
417 }
418 }
419
420 if let Some(ref mut p) = self.pipeline {
421 if !p.swap() {
422 break;
423 }
424 }
425 }
426 }
427
428 /// Add a force field to the scene.
429 pub fn add_field(&mut self, field: ForceField) -> scene::FieldId {
430 self.scene.add_field(field)
431 }
432
433 /// Remove a force field.
434 pub fn remove_field(&mut self, id: scene::FieldId) {
435 self.scene.remove_field(id)
436 }
437
438 /// Spawn a glyph into the scene.
439 pub fn spawn_glyph(&mut self, glyph: Glyph) -> glyph::GlyphId {
440 self.scene.spawn_glyph(glyph)
441 }
442
443 /// Spawn an amorphous entity, creating its formation glyphs.
444 pub fn spawn_entity(&mut self, mut entity: AmorphousEntity) -> entity::EntityId {
445 // If no formation was specified, generate a default diamond
446 if entity.formation.is_empty() {
447 use entity::formation::Formation;
448 let f = Formation::diamond(2);
449 entity.formation = f.positions;
450 entity.formation_chars = f.chars;
451 }
452 // Ensure colors are filled (white if unspecified)
453 while entity.formation_colors.len() < entity.formation.len() {
454 entity.formation_colors.push(glam::Vec4::ONE);
455 }
456 // Spawn one glyph per formation slot
457 for i in 0..entity.formation.len() {
458 let offset = entity.formation[i];
459 let ch = entity.formation_chars.get(i).copied().unwrap_or('◆');
460 let color = entity.formation_colors.get(i).copied().unwrap_or(glam::Vec4::ONE);
461 let id = self.scene.spawn_glyph(Glyph {
462 character: ch,
463 position: entity.position + offset,
464 color,
465 emission: 0.8,
466 glow_color: glam::Vec3::new(color.x, color.y, color.z),
467 glow_radius: 1.2,
468 mass: entity.entity_mass / entity.formation.len().max(1) as f32,
469 layer: RenderLayer::Entity,
470 ..Default::default()
471 });
472 entity.glyph_ids.push(id);
473 }
474 self.scene.spawn_entity(entity)
475 }
476
477 /// Emit a burst of particles at a position.
478 pub fn emit_particles(&mut self, emitter: particle::EmitterPreset, origin: glam::Vec3) {
479 particle::emit(&mut self.scene, emitter, origin);
480 }
481
482 /// Apply trauma (screen shake). 0.0 = none, 1.0 = maximum.
483 pub fn add_trauma(&mut self, amount: f32) {
484 self.camera.add_trauma(amount);
485 }
486}
487
488/// Request quit on next frame.
489impl ProofEngine {
490 /// Ask the run loop to exit at the end of the current frame.
491 pub fn request_quit(&mut self) {
492 self.input.quit_requested = true;
493 }
494
495 /// Get a reference to the glow GL context (for egui integration).
496 /// Returns None if the pipeline hasn't been initialized yet.
497 pub fn gl(&self) -> Option<&glow::Context> {
498 self.pipeline.as_ref().map(|p| p.gl())
499 }
500
501 /// Get the window reference (for egui-winit event processing).
502 pub fn window(&self) -> Option<&winit::window::Window> {
503 self.pipeline.as_ref().map(|p| p.window())
504 }
505
506 /// Get the current window size in pixels.
507 pub fn window_size(&self) -> (u32, u32) {
508 self.pipeline.as_ref().map(|p| p.window_size()).unwrap_or((1600, 1000))
509 }
510
511 /// The framebuffer size, in the same units the viewport uses.
512 ///
513 /// Screen-space UI must lay out against this, not the window size: on a
514 /// scaled display the two differ and the UI ends up magnified.
515 pub fn render_size(&self) -> (u32, u32) {
516 self.pipeline.as_ref().map(|p| p.render_size()).unwrap_or((1600, 1000))
517 }
518
519 /// The frame currently in the back buffer, as RGBA8 with the top row
520 /// first, or `None` before the window exists.
521 ///
522 /// Read straight off the GPU after post-processing, so it is exactly
523 /// what the pipeline drew. Asking the window manager for a picture of a
524 /// hardware-accelerated window is unreliable: it hands back whatever it
525 /// last cached, which can be a stale frame or a blank one.
526 pub fn frame_pixels(&self) -> Option<(u32, u32, Vec<u8>)> {
527 let p = self.pipeline.as_ref()?;
528 let (w, h, mut rgba) = p.read_frame();
529 if w == 0 || h == 0 {
530 return None;
531 }
532 export::flip_rows(w, h, &mut rgba);
533 Some((w, h, rgba))
534 }
535
536 /// Write the frame currently on screen to an image file.
537 ///
538 /// The extension picks the format (`.png`, `.jpg`, `.bmp`, `.tga`,
539 /// `.gif`); an unknown extension writes a BMP, as this always did
540 /// before. The image is opaque. See [`export`] for animated GIFs and for
541 /// saving buffers of your own.
542 pub fn save_frame(&self, path: &str) -> std::io::Result<()> {
543 let (w, h, rgba) = self.frame_pixels().ok_or_else(|| {
544 std::io::Error::new(std::io::ErrorKind::Other, "no frame to read: the window is not open")
545 })?;
546 export::save_opaque(path, w, h, &rgba)
547 }
548}
549
550/// Common imports for using Proof Engine.
551pub mod prelude {
552 pub use crate::{
553 ProofEngine, EngineConfig,
554 MathFunction, ForceField, Falloff, AttractorType,
555 Glyph, RenderLayer, BlendMode,
556 AmorphousEntity,
557 MathParticle, ParticleInteraction,
558 AudioEvent,
559 particle::EmitterPreset,
560 render::camera::ProofCamera,
561 input::{InputState, Key},
562 scene::{SceneGraph, FieldId},
563 audio::MusicVibe,
564 tween::{Tween, Easing, TweenState, Tweens, AnimationGroup},
565 tween::easing::Easing as EasingFn,
566 tween::keyframe::{KeyframeTrack, Keyframe, CameraPath, ExtrapolateMode},
567 tween::sequence::{TweenSequence, TweenTimeline, SequenceBuilder},
568 debug::DebugOverlay,
569 render::pipeline::FrameStats,
570 render::screen_fx::{ScreenFx, ScreenLight, Shockwave},
571 render::ui_layer::UiPass,
572 };
573 // Quat and Mat4 belong here too: the skeleton and animation APIs hand out
574 // transforms built from them, so a caller who only has the prelude cannot
575 // pose a figure without reaching past it into glam.
576 pub use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
577}