1use cuneus::compute::ComputeShader;
2use cuneus::{
3 Core, ExportManager, RenderKit, ShaderControls, ShaderManager,
4};
5use log::debug;
6use cuneus::WindowEvent;
7
8cuneus::uniform_params! {
9 struct MandelbulbParams {
10 power: f32,
11 max_bounces: u32,
12 samples_per_pixel: u32,
13 accumulate: u32,
14
15 animation_speed: f32,
16 hold_duration: f32,
17 transition_duration: f32,
18
19 exposure: f32,
20 focal_length: f32,
21 dof_strength: f32,
22
23 palette_a_r: f32,
24 palette_a_g: f32,
25 palette_a_b: f32,
26 palette_b_r: f32,
27 palette_b_g: f32,
28 palette_b_b: f32,
29 palette_c_r: f32,
30 palette_c_g: f32,
31 palette_c_b: f32,
32 palette_d_r: f32,
33 palette_d_g: f32,
34 palette_d_b: f32,
35
36 gamma: f32,
37 zoom: f32,
38
39 background_r: f32,
40 background_g: f32,
41 background_b: f32,
42 sun_color_r: f32,
43 sun_color_g: f32,
44 sun_color_b: f32,
45 fog_color_r: f32,
46 fog_color_g: f32,
47 fog_color_b: f32,
48 glow_color_r: f32,
49 glow_color_g: f32,
50 glow_color_b: f32,
51
52 rotation_x: f32,
53 rotation_y: f32,
54 rotation_z: f32,
55 _pad: f32,
56}
57}
58
59struct MandelbulbShader {
60 base: RenderKit,
61 compute_shader: ComputeShader,
62 frame_count: u32,
63 should_reset_accumulation: bool,
64 current_params: MandelbulbParams,
65 previous_mouse_pos: [f32; 2],
67 mouse_enabled: bool,
68 mouse_initialized: bool,
69 accumulated_rotation: [f32; 3],
71 accumulated_zoom: f32,
73}
74
75impl MandelbulbShader {
76 fn reset_accumulation(&mut self) {
77 self.compute_shader.current_frame = 0;
78 self.should_reset_accumulation = false;
79 self.frame_count = 0;
80 }
81}
82
83impl ShaderManager for MandelbulbShader {
84 fn init(core: &Core) -> Self {
85 let initial_params = MandelbulbParams {
86 power: 4.0,
87 max_bounces: 2,
88 samples_per_pixel: 1,
89 accumulate: 1,
90
91 animation_speed: 1.0,
92 hold_duration: 3.0,
93 transition_duration: 3.0,
94
95 exposure: 1.0,
96 focal_length: 2.5,
97 dof_strength: 0.04,
98
99 palette_a_r: 0.5,
100 palette_a_g: 0.7,
101 palette_a_b: 0.5,
102 palette_b_r: 0.9,
103 palette_b_g: 0.8,
104 palette_b_b: 0.1,
105 palette_c_r: 1.0,
106 palette_c_g: 1.0,
107 palette_c_b: 1.0,
108 palette_d_r: 1.0,
109 palette_d_g: 1.15,
110 palette_d_b: 0.20,
111
112 gamma: 1.1,
113 zoom: 1.0,
114
115 background_r: 0.05,
116 background_g: 0.1,
117 background_b: 0.15,
118 sun_color_r: 8.10,
119 sun_color_g: 6.00,
120 sun_color_b: 4.20,
121 fog_color_r: 0.05,
122 fog_color_g: 0.1,
123 fog_color_b: 0.15,
124 glow_color_r: 0.5,
125 glow_color_g: 0.7,
126 glow_color_b: 1.0,
127
128 rotation_x: 0.0,
129 rotation_y: 0.0,
130 rotation_z: 0.0,
131 _pad: 0.0,
132 };
133 let base = RenderKit::new(core);
134
135 let passes = vec![
139 cuneus::compute::PassDescription::new("accumulate", &["accumulate"]),
140 cuneus::compute::PassDescription::new("main_image", &["accumulate"]),
141 ];
142
143 let config = ComputeShader::builder()
144 .with_entry_point("accumulate")
145 .with_multi_pass(&passes)
146 .with_custom_uniforms::<MandelbulbParams>()
147 .with_mouse() .with_workgroup_size([16, 16, 1])
149 .with_texture_format(cuneus::compute::COMPUTE_TEXTURE_FORMAT_RGBA16)
150 .with_label("Mandelbulb Unified")
151 .build();
152
153 let compute_shader = cuneus::compute_shader!(core, "shaders/mandelbulb.wgsl", config);
154
155 compute_shader.set_custom_params(initial_params, &core.queue);
157
158 Self {
159 base,
160 compute_shader,
161 frame_count: 0,
162 should_reset_accumulation: true,
163 current_params: initial_params,
164 previous_mouse_pos: [0.5, 0.5],
165 mouse_enabled: false,
166 mouse_initialized: false,
167 accumulated_rotation: [0.0, 0.0, 0.0],
168 accumulated_zoom: 1.0,
169 }
170 }
171
172 fn update(&mut self, core: &Core) {
173
174 self.compute_shader.handle_export(core, &mut self.base);
176 }
177
178 fn resize(&mut self, core: &Core) {
179 self.base.default_resize(core, &mut self.compute_shader);
180 debug!("Resizing to {:?}", core.size);
181 }
182
183 fn render(&mut self, core: &Core) -> Result<(), cuneus::SurfaceError> {
184 let mut frame = self.base.begin_frame(core)?;
185
186 let current_mouse_pos = self.base.mouse_tracker.uniform.position;
187 let mouse_wheel = self.base.mouse_tracker.uniform.wheel;
188
189 if mouse_wheel[1].abs() > 0.001 {
190 let zoom_sensitivity = 0.1;
191 self.accumulated_zoom *= 1.0 - mouse_wheel[1] * zoom_sensitivity;
192 self.accumulated_zoom = self.accumulated_zoom.clamp(0.2, 5.0);
193 self.should_reset_accumulation = true;
194 }
195
196 if self.mouse_enabled {
197 if !self.mouse_initialized {
198 self.previous_mouse_pos = current_mouse_pos;
199 self.mouse_initialized = true;
200 } else {
201 let delta_x: f32 = current_mouse_pos[0] - self.previous_mouse_pos[0];
202 let delta_y = current_mouse_pos[1] - self.previous_mouse_pos[1];
203 if delta_x.abs() > 0.0001 || delta_y.abs() > 0.0001 {
204 let base_sensitivity = 5.0;
205 let aspect = core.size.width as f32 / core.size.height as f32;
206 self.accumulated_rotation[0] += delta_x * base_sensitivity;
207 self.accumulated_rotation[1] += delta_y * base_sensitivity * aspect;
208 self.should_reset_accumulation = true;
209 self.previous_mouse_pos = current_mouse_pos;
210 }
211 }
212 }
213
214 self.base.mouse_tracker.reset_wheel();
215
216 let mut params = self.current_params;
217 let mut changed = false;
218 let mut should_start_export = false;
219 let mut export_request = self.base.export_manager.get_ui_request();
220 let mut controls_request = self
221 .base
222 .controls
223 .get_ui_request(&self.base.start_time, &core.size, self.base.fps_tracker.fps());
224
225 let current_fps = self.base.fps_tracker.fps();
226
227 let full_output = if self.base.key_handler.show_ui {
228 self.base.render_ui(core, |ctx| {
229 RenderKit::apply_default_style(ctx);
230
231 egui::Window::new("Mandelbulb PathTracer")
232 .collapsible(true)
233 .resizable(true)
234 .default_width(350.0)
235 .show(ctx, |ui| {
236 ui.label("WASD: Rotate | QE: Roll | Scroll: Zoom");
237 ui.separator();
238
239 egui::CollapsingHeader::new("Camera&View")
240 .default_open(false)
241 .show(ui, |ui| {
242 if ui.add(egui::Slider::new(&mut self.accumulated_zoom, 0.2..=5.0).text("Zoom")).changed() {
243 self.should_reset_accumulation = true;
244 }
245 changed |= ui
246 .add(
247 egui::Slider::new(&mut params.focal_length, 2.0..=20.0)
248 .text("Focal Length"),
249 )
250 .changed();
251 changed |= ui
252 .add(
253 egui::Slider::new(&mut params.dof_strength, 0.0..=1.0)
254 .text("DoF"),
255 )
256 .changed();
257
258 ui.separator();
259 let old_mouse_enabled = self.mouse_enabled;
260 ui.checkbox(&mut self.mouse_enabled, "Mouse Camera Control (M key)");
261 if self.mouse_enabled != old_mouse_enabled {
262 self.mouse_initialized = false;
263 }
264 if !self.mouse_enabled {
265 ui.colored_label(
266 egui::Color32::GRAY,
267 "Mouse disabled - camera locked",
268 );
269 } else {
270 ui.colored_label(egui::Color32::GREEN, "Mouse active");
271 }
272 ui.horizontal(|ui| {
273 if ui.button("Reset Rotation").clicked() {
274 self.accumulated_rotation = [0.0, 0.0, 0.0];
275 self.should_reset_accumulation = true;
276 }
277 if ui.button("Reset Zoom").clicked() {
278 self.accumulated_zoom = 1.0;
279 self.should_reset_accumulation = true;
280 }
281 });
282 });
283
284 egui::CollapsingHeader::new("Mandelbulb")
285 .default_open(false)
286 .show(ui, |ui| {
287 let old_power = params.power;
288 changed |= ui
289 .add(
290 egui::Slider::new(&mut params.power, 2.0..=12.0)
291 .text("Power"),
292 )
293 .changed();
294 if params.power != old_power {
295 self.should_reset_accumulation = true;
296 }
297 });
298
299 egui::CollapsingHeader::new("Render")
300 .default_open(false)
301 .show(ui, |ui| {
302 let old_samples = params.samples_per_pixel;
303 changed |= ui
304 .add(
305 egui::Slider::new(&mut params.samples_per_pixel, 1..=8)
306 .text("Samples/pixel"),
307 )
308 .changed();
309 if params.samples_per_pixel != old_samples {
310 self.should_reset_accumulation = true;
311 }
312
313 let old_bounces = params.max_bounces;
314 changed |= ui
315 .add(
316 egui::Slider::new(&mut params.max_bounces, 1..=12)
317 .text("Max Bounces"),
318 )
319 .changed();
320 if params.max_bounces != old_bounces {
321 self.should_reset_accumulation = true;
322 }
323
324 let old_accumulate = params.accumulate;
325 let mut accumulate_bool = params.accumulate > 0;
326 changed |= ui
327 .checkbox(&mut accumulate_bool, "Progressive Rendering")
328 .changed();
329 params.accumulate = if accumulate_bool { 1 } else { 0 };
330 if params.accumulate != old_accumulate {
331 self.should_reset_accumulation = true;
332 }
333
334 changed |= ui
335 .add(
336 egui::Slider::new(&mut params.exposure, 0.1..=5.0)
337 .text("Exposure"),
338 )
339 .changed();
340 changed |= ui
341 .add(
342 egui::Slider::new(&mut params.gamma, 0.1..=2.0)
343 .text("Gamma"),
344 )
345 .changed();
346
347 if ui.button("Reset Accumulation").clicked() {
348 self.should_reset_accumulation = true;
349 changed = true;
350 }
351 });
352
353 egui::CollapsingHeader::new("env")
354 .default_open(false)
355 .show(ui, |ui| {
356 ui.horizontal(|ui| {
357 ui.label("bg:");
358 let mut bg_color = [
359 params.background_r,
360 params.background_g,
361 params.background_b,
362 ];
363 if ui.color_edit_button_rgb(&mut bg_color).changed() {
364 params.background_r = bg_color[0];
365 params.background_g = bg_color[1];
366 params.background_b = bg_color[2];
367 changed = true;
368 }
369 });
370
371 ui.horizontal(|ui| {
372 ui.label("Sun:");
373 let mut sun_color = [
374 params.sun_color_r,
375 params.sun_color_g,
376 params.sun_color_b,
377 ];
378 if ui.color_edit_button_rgb(&mut sun_color).changed() {
379 params.sun_color_r = sun_color[0];
380 params.sun_color_g = sun_color[1];
381 params.sun_color_b = sun_color[2];
382 changed = true;
383 }
384 });
385
386 ui.horizontal(|ui| {
387 ui.label("Fog:");
388 let mut fog_color = [
389 params.fog_color_r,
390 params.fog_color_g,
391 params.fog_color_b,
392 ];
393 if ui.color_edit_button_rgb(&mut fog_color).changed() {
394 params.fog_color_r = fog_color[0];
395 params.fog_color_g = fog_color[1];
396 params.fog_color_b = fog_color[2];
397 changed = true;
398 }
399 });
400
401 ui.horizontal(|ui| {
402 ui.label("Sky Glow:");
403 let mut glow_color = [
404 params.glow_color_r,
405 params.glow_color_g,
406 params.glow_color_b,
407 ];
408 if ui.color_edit_button_rgb(&mut glow_color).changed() {
409 params.glow_color_r = glow_color[0];
410 params.glow_color_g = glow_color[1];
411 params.glow_color_b = glow_color[2];
412 changed = true;
413 }
414 });
415
416 if ui.button("Reset env cols").clicked() {
417 params.background_r = 0.1;
418 params.background_g = 0.1;
419 params.background_b = 0.15;
420 params.sun_color_r = 8.10;
421 params.sun_color_g = 6.00;
422 params.sun_color_b = 4.20;
423 params.fog_color_r = 0.1;
424 params.fog_color_g = 0.1;
425 params.fog_color_b = 0.15;
426 params.glow_color_r = 0.5;
427 params.glow_color_g = 0.7;
428 params.glow_color_b = 1.0;
429 changed = true;
430 }
431 });
432
433 egui::CollapsingHeader::new("Color Palette")
434 .default_open(false)
435 .show(ui, |ui| {
436 ui.horizontal(|ui| {
437 ui.label("Base Color:");
438 let mut color_a = [
439 params.palette_a_r,
440 params.palette_a_g,
441 params.palette_a_b,
442 ];
443 if ui.color_edit_button_rgb(&mut color_a).changed() {
444 params.palette_a_r = color_a[0];
445 params.palette_a_g = color_a[1];
446 params.palette_a_b = color_a[2];
447 changed = true;
448 }
449 });
450
451 ui.horizontal(|ui| {
452 ui.label("Amplitude:");
453 let mut color_b = [
454 params.palette_b_r,
455 params.palette_b_g,
456 params.palette_b_b,
457 ];
458 if ui.color_edit_button_rgb(&mut color_b).changed() {
459 params.palette_b_r = color_b[0];
460 params.palette_b_g = color_b[1];
461 params.palette_b_b = color_b[2];
462 changed = true;
463 }
464 });
465
466 ui.horizontal(|ui| {
467 ui.label("Frequency:");
468 let mut color_c = [
469 params.palette_c_r,
470 params.palette_c_g,
471 params.palette_c_b,
472 ];
473 if ui.color_edit_button_rgb(&mut color_c).changed() {
474 params.palette_c_r = color_c[0];
475 params.palette_c_g = color_c[1];
476 params.palette_c_b = color_c[2];
477 changed = true;
478 }
479 });
480
481 ui.horizontal(|ui| {
482 ui.label("Phase:");
483 let mut color_d = [
484 params.palette_d_r,
485 params.palette_d_g,
486 params.palette_d_b,
487 ];
488 if ui.color_edit_button_rgb(&mut color_d).changed() {
489 params.palette_d_r = color_d[0];
490 params.palette_d_g = color_d[1];
491 params.palette_d_b = color_d[2];
492 changed = true;
493 }
494 });
495 if ui.button("Reset to Default Palette").clicked() {
496 params.palette_a_r = 0.5;
497 params.palette_a_g = 0.5;
498 params.palette_a_b = 0.5;
499 params.palette_b_r = 0.5;
500 params.palette_b_g = 0.1;
501 params.palette_b_b = 0.1;
502 params.palette_c_r = 1.0;
503 params.palette_c_g = 1.0;
504 params.palette_c_b = 1.0;
505 params.palette_d_r = 0.0;
506 params.palette_d_g = 0.33;
507 params.palette_d_b = 0.67;
508 changed = true;
509 }
510
511 ui.separator();
512 });
513
514 ui.separator();
515
516 ShaderControls::render_controls_widget(ui, &mut controls_request);
517
518 ui.separator();
519
520 should_start_export =
521 ExportManager::render_export_ui_widget(ui, &mut export_request);
522
523 ui.separator();
524 ui.label(format!("Accumulated Samples: {}", self.frame_count));
525 ui.label(format!(
526 "Resolution: {}x{}",
527 core.size.width, core.size.height
528 ));
529 ui.label(format!("FPS: {current_fps:.1}"));
530 });
531 })
532 } else {
533 self.base.render_ui(core, |_ctx| {})
534 };
535
536 self.base.export_manager.apply_ui_request(export_request);
537 if controls_request.should_clear_buffers || self.should_reset_accumulation {
538 self.reset_accumulation();
539 }
540 self.base.apply_control_request(controls_request);
541
542 let current_time = self.base.controls.get_time(&self.base.start_time);
543
544 self.base.time_uniform.data.time = current_time;
545 self.base.time_uniform.data.frame = self.frame_count;
546 self.base.time_uniform.update(&core.queue);
547
548 self.compute_shader
550 .set_time(current_time, 1.0 / 60.0, &core.queue);
551 self.compute_shader.time_uniform.data.frame = self.frame_count;
552 self.compute_shader.time_uniform.update(&core.queue);
553
554 if changed {
555 self.current_params = params;
556 self.should_reset_accumulation = true;
557 }
558
559 self.current_params.rotation_x = self.accumulated_rotation[0];
560 self.current_params.rotation_y = -self.accumulated_rotation[1];
561 self.current_params.rotation_z = self.accumulated_rotation[2];
562 self.current_params.zoom = self.accumulated_zoom;
563 self.compute_shader.set_custom_params(self.current_params, &core.queue);
564
565 if should_start_export {
566 self.base.export_manager.start_export();
567 }
568
569 self.compute_shader.dispatch(&mut frame.encoder, core);
570
571 self.base.renderer.render_to_view(&mut frame.encoder, &frame.view, &self.compute_shader.get_output_texture().bind_group);
572
573 self.base.end_frame(core, frame, full_output);
574
575 if self.current_params.accumulate > 0 {
576 self.frame_count += 1;
577 }
578
579 Ok(())
580 }
581
582 fn handle_input(&mut self, core: &Core, event: &WindowEvent) -> bool {
583 if self
584 .base
585 .egui_state
586 .on_window_event(core.window(), event)
587 .consumed
588 {
589 return true;
590 }
591
592 if self.base.handle_mouse_input(core, event, false) {
593 return true;
594 }
595
596 if let WindowEvent::KeyboardInput { event, .. } = event {
597 if let winit::keyboard::Key::Character(ch) = &event.logical_key {
598 match ch.as_str() {
599 " " => {
600 if event.state == winit::event::ElementState::Released {
601 self.current_params.accumulate = 1 - self.current_params.accumulate;
602 self.should_reset_accumulation = true;
603 self.compute_shader
604 .set_custom_params(self.current_params, &core.queue);
605 return true;
606 }
607 }
608 "m" | "M" => {
609 if event.state == winit::event::ElementState::Released {
610 self.mouse_enabled = !self.mouse_enabled;
611 self.mouse_initialized = false;
612 return true;
613 }
614 }
615 "w" | "W" => {
616 if event.state == winit::event::ElementState::Pressed {
617 self.accumulated_rotation[1] -= 0.1;
618 self.should_reset_accumulation = true;
619 return true;
620 }
621 }
622 "s" | "S" => {
623 if event.state == winit::event::ElementState::Pressed {
624 self.accumulated_rotation[1] += 0.1;
625 self.should_reset_accumulation = true;
626 return true;
627 }
628 }
629 "a" | "A" => {
630 if event.state == winit::event::ElementState::Pressed {
631 self.accumulated_rotation[0] -= 0.1;
632 self.should_reset_accumulation = true;
633 return true;
634 }
635 }
636 "d" | "D" => {
637 if event.state == winit::event::ElementState::Pressed {
638 self.accumulated_rotation[0] += 0.1;
639 self.should_reset_accumulation = true;
640 return true;
641 }
642 }
643 "q" | "Q" => {
644 if event.state == winit::event::ElementState::Pressed {
645 self.accumulated_rotation[2] -= 0.1;
646 self.should_reset_accumulation = true;
647 return true;
648 }
649 }
650 "e" | "E" => {
651 if event.state == winit::event::ElementState::Pressed {
652 self.accumulated_rotation[2] += 0.1;
653 self.should_reset_accumulation = true;
654 return true;
655 }
656 }
657 _ => {}
658 }
659 }
660 }
661
662 if let WindowEvent::KeyboardInput { event, .. } = event {
663 if self
664 .base
665 .key_handler
666 .handle_keyboard_input(core.window(), event)
667 {
668 return true;
669 }
670 }
671
672 false
673 }
674}
675
676fn main() -> Result<(), Box<dyn std::error::Error>> {
677 env_logger::init();
678 let (app, event_loop) = cuneus::ShaderApp::new("Mandelbulb Path Tracer", 600, 400);
679
680 app.run(event_loop, MandelbulbShader::init)
681}