Skip to main content

physarum/
physarum.rs

1use cuneus::compute::*;
2use cuneus::prelude::*;
3
4cuneus::uniform_params! {
5    struct PhysarumParams {
6        sensor_angle: f32, sensor_dist: f32, drag: f32, move_speed: f32,
7        decay_rate: f32, diffuse_rate: f32, deposit_amt: f32, random_jitter: f32,
8        rule_seed: f32, mutation_scale: f32, force_scale: f32, sensor_gain: f32,
9        species_attract: f32, species_repel: f32, strafe_power: f32, agent_count_scale: f32,
10        glow_intensity: f32, color_shift: f32, specular_strength: f32, gamma: f32,
11        attractor_count: f32, attractor_speed: f32, attractor_radius: f32, attractor_strength: f32,
12        color_spread: f32, saturation: f32, palette_mix: f32, blur_samples: f32,
13        color0_r: f32, color0_g: f32, color0_b: f32,
14        color1_r: f32, color1_g: f32, color1_b: f32,
15        color2_r: f32, color2_g: f32, color2_b: f32,
16        substep: f32, total_samples: f32, wind_strength: f32, turing_scale: f32,
17        _pad1: f32, fluid_blend: f32, _pad3: f32,
18    }
19}
20
21struct PhysarumShader {
22    base: RenderKit,
23    compute_shader: ComputeShader,
24    current_params: PhysarumParams,
25}
26
27impl ShaderManager for PhysarumShader {
28    fn init(core: &Core) -> Self {
29        let initial_params = PhysarumParams {
30            sensor_angle: 1.50, sensor_dist: 40.0, drag: 0.20, move_speed: 4.95,
31            decay_rate: 0.999, diffuse_rate: 0.10, deposit_amt: 30.0, random_jitter: 0.500,
32            rule_seed: 1.0, mutation_scale: 0.30, force_scale: 0.80, sensor_gain: 5.0,
33            species_attract: 1.00, species_repel: 0.92, strafe_power: 0.15, agent_count_scale: 1.00,
34            glow_intensity: 0.00, color_shift: -0.02, specular_strength: 0.25, gamma: 0.50,
35            attractor_count: 3.0, attractor_speed: 0.30, attractor_radius: 250.0, attractor_strength: 0.15,
36            color_spread: 0.30, saturation: 1.58, palette_mix: 0.57, blur_samples: 0.99,
37            color0_r: 1.0, color0_g: 0.2, color0_b: 0.2,
38            color1_r: 0.2, color1_g: 0.8, color1_b: 0.3,
39            color2_r: 0.2, color2_g: 0.4, color2_b: 1.0,
40            substep: 1.0, total_samples: 1.0, wind_strength: 0.8, turing_scale: 0.0,
41            _pad1: 0.0, fluid_blend: 0.0, _pad3: 0.0,
42        };
43
44        let base = RenderKit::new(core);
45
46        let passes = vec![
47            PassDescription::new("agent_update", &["agent_update", "turing_resolve"]).with_resolution(1024, 1024),
48            PassDescription::new("process_trails", &["process_trails"]),
49            PassDescription::new("diffuse_h", &["process_trails"]),
50            PassDescription::new("diffuse_v", &["process_trails", "diffuse_h"]),
51            PassDescription::new("inhibitor_down", &["diffuse_v"]).with_resolution_scale(0.125),
52            PassDescription::new("turing_resolve", &["process_trails", "diffuse_v", "inhibitor_down"]),
53            PassDescription::new("main_image", &["process_trails", "turing_resolve", "inhibitor_down"]),
54        ];
55
56        let config = ComputeShader::builder()
57            .with_multi_pass(&passes)
58            .with_custom_uniforms::<PhysarumParams>()
59            .with_atomic_buffer(4)
60            .with_label("Physarum Simulation")
61            .build();
62
63        let compute_shader = cuneus::compute_shader!(core, "shaders/physarum.wgsl", config);
64        compute_shader.set_custom_params(initial_params, &core.queue);
65
66        Self { base, compute_shader, current_params: initial_params }
67    }
68
69    fn update(&mut self, core: &Core) {
70        let current_time = self.base.controls.get_time(&self.base.start_time);
71        self.compute_shader.set_time(current_time, 1.0 / 60.0, &core.queue);
72        self.compute_shader.handle_export(core, &mut self.base);
73    }
74
75    fn resize(&mut self, core: &Core) {
76        self.base.default_resize(core, &mut self.compute_shader);
77    }
78
79    fn render(&mut self, core: &Core) -> Result<(), cuneus::SurfaceError> {
80        let mut frame = self.base.begin_frame(core)?;
81        let mut params = self.current_params;
82        let mut changed = false;
83        let mut should_start_export = false;
84        let mut export_request = self.base.export_manager.get_ui_request();
85        let mut controls_request = self.base.controls.get_ui_request(
86            &self.base.start_time, &core.size, self.base.fps_tracker.fps()
87        );
88
89        let full_output = if self.base.key_handler.show_ui {
90            self.base.render_ui(core, |ctx| {
91                RenderKit::apply_default_style(ctx);
92                egui::Window::new("Physarum Controls")
93                    .collapsible(true).resizable(true).default_width(320.0)
94                    .show(ctx, |ui| {
95                        egui::CollapsingHeader::new("Behavior Rule").default_open(false).show(ui, |ui| {
96                            changed |= ui.add(egui::Slider::new(&mut params.rule_seed, 0.0..=100.0).text("Rule Seed")).changed();
97                            changed |= ui.add(egui::Slider::new(&mut params.mutation_scale, 0.0..=1.0).text("Species Diversity")).changed();
98                            changed |= ui.add(egui::Slider::new(&mut params.force_scale, 0.0..=3.0).text("Force Scale")).changed();
99                            changed |= ui.add(egui::Slider::new(&mut params.sensor_gain, 0.5..=30.0).text("Sensor Gain")).changed();
100                            changed |= ui.add(egui::Slider::new(&mut params.strafe_power, 0.0..=2.0).text("Strafe Power")).changed();
101                        });
102
103                        egui::CollapsingHeader::new("Agent Physics").default_open(false).show(ui, |ui| {
104                            changed |= ui.add(egui::Slider::new(&mut params.move_speed, 0.1..=5.0).text("Speed")).changed();
105                            changed |= ui.add(egui::Slider::new(&mut params.drag, 0.0..=0.99).text("Momentum")).changed();
106                            changed |= ui.add(egui::Slider::new(&mut params.wind_strength, 0.0..=3.0).text("Slipstream Wind")).changed();
107                            changed |= ui.add(egui::Slider::new(&mut params.fluid_blend, 0.0..=2.0).text("Bubble vs Vein Bias")).changed();
108                            changed |= ui.add(egui::Slider::new(&mut params.sensor_dist, 1.0..=40.0).text("Sensor Distance")).changed();
109                            changed |= ui.add(egui::Slider::new(&mut params.sensor_angle, 0.05..=1.5).text("Sensor Angle")).changed();
110                            changed |= ui.add(egui::Slider::new(&mut params.random_jitter, 0.0..=0.5).text("Random Jitter")).changed();
111                            changed |= ui.add(egui::Slider::new(&mut params.agent_count_scale, 0.05..=1.0).text("Agent Density")).changed();
112                        });
113
114                        egui::CollapsingHeader::new("Trail Environment").default_open(false).show(ui, |ui| {
115                            changed |= ui.add(egui::Slider::new(&mut params.deposit_amt, 1.0..=30.0).text("Deposit Amount")).changed();
116                            changed |= ui.add(egui::Slider::new(&mut params.decay_rate, 0.9..=0.999).text("Decay Rate")).changed();
117                            changed |= ui.add(egui::Slider::new(&mut params.diffuse_rate, 0.0..=1.0).text("Diffusion Rate")).changed();
118                        });
119
120                        egui::CollapsingHeader::new("Species Interaction").default_open(false).show(ui, |ui| {
121                            changed |= ui.add(egui::Slider::new(&mut params.species_attract, 0.0..=1.0).text("Cross-Attraction")).changed();
122                            changed |= ui.add(egui::Slider::new(&mut params.species_repel, 0.0..=1.0).text("Cross-Repulsion")).changed();
123                        });
124
125                        egui::CollapsingHeader::new("Attractors").default_open(false).show(ui, |ui| {
126                            changed |= ui.add(egui::Slider::new(&mut params.attractor_count, 0.0..=8.0).step_by(1.0).text("Count")).changed();
127                            changed |= ui.add(egui::Slider::new(&mut params.attractor_speed, 0.0..=2.0).text("Orbit Speed")).changed();
128                            changed |= ui.add(egui::Slider::new(&mut params.attractor_radius, 50.0..=500.0).text("Radius")).changed();
129                            changed |= ui.add(egui::Slider::new(&mut params.attractor_strength, 0.0..=1.0).text("Strength")).changed();
130                        });
131
132                        egui::CollapsingHeader::new("Rendering").default_open(false).show(ui, |ui| {
133                            changed |= ui.add(egui::Slider::new(&mut params.blur_samples, 0.0..=1.0).text("Feedback Fade")).changed();
134                            changed |= ui.add(egui::Slider::new(&mut params.glow_intensity, 0.0..=2.0).text("Glow")).changed();
135                            changed |= ui.add(egui::Slider::new(&mut params.specular_strength, 0.0..=1.5).text("Specular")).changed();
136                            changed |= ui.add(egui::Slider::new(&mut params.gamma, 0.1..=2.2).text("Gamma")).changed();
137                        });
138
139                        egui::CollapsingHeader::new("Colors").default_open(false).show(ui, |ui| {
140                            changed |= ui.add(egui::Slider::new(&mut params.palette_mix, 0.0..=1.0).text("Palette Mix")).changed();
141                            changed |= ui.add(egui::Slider::new(&mut params.color_shift, -0.5..=0.5).text("Color Shift")).changed();
142                            changed |= ui.add(egui::Slider::new(&mut params.color_spread, 0.0..=1.0).text("Species Hue Spread")).changed();
143                            changed |= ui.add(egui::Slider::new(&mut params.saturation, 0.0..=2.5).text("Saturation")).changed();
144                        });
145
146                        ui.separator();
147                        ShaderControls::render_controls_widget(ui, &mut controls_request);
148                        ui.separator();
149                        should_start_export = ExportManager::render_export_ui_widget(ui, &mut export_request);
150                    });
151            })
152        } else {
153            self.base.render_ui(core, |_ctx| {})
154        };
155
156        if controls_request.should_clear_buffers { self.compute_shader.current_frame = 0; }
157        if !self.base.export_manager.is_exporting() { self.compute_shader.dispatch(&mut frame.encoder, core); }
158
159        self.base.renderer.render_to_view(
160            &mut frame.encoder,
161            &frame.view,
162            &self.compute_shader.get_output_texture().bind_group,
163        );
164        self.base.apply_control_request(controls_request);
165        self.base.export_manager.apply_ui_request(export_request);
166
167        if should_start_export { self.base.export_manager.start_export(); }
168        if changed {
169            self.current_params = params;
170            self.compute_shader.set_custom_params(params, &core.queue);
171        }
172
173        self.base.end_frame(core, frame, full_output);
174        Ok(())
175    }
176
177    fn handle_input(&mut self, core: &Core, event: &WindowEvent) -> bool {
178        self.base.default_handle_input(core, event)
179    }
180}
181
182fn main() -> Result<(), Box<dyn std::error::Error>> {
183    env_logger::init();
184    let (app, event_loop) = ShaderApp::new("Physarum Engine", 1280, 720);
185    app.run(event_loop, PhysarumShader::init)
186}