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}