Skip to main content

rorschach/
rorschach.rs

1use cuneus::compute::*;
2use cuneus::prelude::*;
3
4cuneus::uniform_params! {
5    struct RorschachParams {
6    seed: f32,
7    zoom: f32,
8    threshold: f32,
9    distortion: f32,
10    
11    particle_speed: f32,
12    particle_life: f32,
13    trace_steps: f32,
14    contrast: f32,
15    
16    color_r: f32,
17    color_g: f32,
18    color_b: f32,
19    gamma: f32,
20
21    style: f32, 
22    fbm_octaves: f32,
23    tint_x: f32,
24    tint_y: f32,
25
26    tint_z: f32,
27    _pad_final1: f32,
28    _pad_final2: f32,
29    _pad_final3: f32}
30}
31
32struct RorschachShader {
33    base: RenderKit,
34    compute_shader: ComputeShader,
35    current_params: RorschachParams}
36
37impl ShaderManager for RorschachShader {
38    fn init(core: &Core) -> Self {
39        let initial_params = RorschachParams {
40            seed: 87.0,
41            zoom: 5.2,
42            threshold: 0.383,
43            distortion: 2.63,
44            particle_speed: 0.45,
45            particle_life: 0.99,
46            trace_steps: 22.0,
47            contrast: 6.0,
48            color_r: 0.58,
49            color_g: 0.12,
50            color_b: 0.12,
51            gamma: 0.2,
52            style: 1.0, 
53            
54            fbm_octaves: 5.0,
55            tint_x: 0.3,
56            tint_y: 0.04,
57            tint_z: 0.28,
58
59            _pad_final1: 0.0,
60            _pad_final2: 0.0,
61            _pad_final3: 0.0};
62        let base = RenderKit::new(core);
63
64        let passes = vec![
65            PassDescription::new("shape", &[]),
66            PassDescription::new("flow_field", &["shape"]),
67            PassDescription::new("ink_trace", &["ink_trace", "flow_field"]),
68            PassDescription::new("main_image", &["ink_trace"]),
69        ];
70
71        let config = ComputeShader::builder()
72            .with_entry_point("shape")
73            .with_multi_pass(&passes)
74            .with_custom_uniforms::<RorschachParams>()
75            .with_workgroup_size([16, 16, 1])
76            .with_texture_format(COMPUTE_TEXTURE_FORMAT_RGBA16)
77            .with_label("Rorschach Unified")
78            .build();
79
80        let compute_shader = cuneus::compute_shader!(core, "shaders/rorschach.wgsl", config);
81
82        compute_shader.set_custom_params(initial_params, &core.queue);
83
84        Self {
85            base,
86            compute_shader,
87            current_params: initial_params}
88    }
89
90    fn update(&mut self, core: &Core) {
91        self.compute_shader.handle_export(core, &mut self.base);
92
93        let current_time = self.base.controls.get_time(&self.base.start_time);
94        let delta = 1.0 / 60.0;
95        self.compute_shader.set_time(current_time, delta, &core.queue);
96    }
97
98    fn resize(&mut self, core: &Core) {
99        self.base.default_resize(core, &mut self.compute_shader);
100    }
101
102    fn render(&mut self, core: &Core) -> Result<(), cuneus::SurfaceError> {
103        let mut frame = self.base.begin_frame(core)?;
104
105        let mut params = self.current_params;
106        let mut changed = false;
107        let mut should_reset = false;
108        let mut should_start_export = false;
109        let mut export_request = self.base.export_manager.get_ui_request();
110        let mut controls_request = self.base.controls.get_ui_request(&self.base.start_time, &core.size, self.base.fps_tracker.fps());
111
112        let full_output = if self.base.key_handler.show_ui {
113            self.base.render_ui(core, |ctx| {
114                ctx.global_style_mut(|style| {
115                    style.visuals.window_fill = egui::Color32::from_rgba_premultiplied(0, 0, 0, 180);
116                    style.text_styles.get_mut(&egui::TextStyle::Body).unwrap().size = 11.0;
117                    style.text_styles.get_mut(&egui::TextStyle::Button).unwrap().size = 10.0;
118                });
119
120                egui::Window::new("Rorschach")
121                    .collapsible(true)
122                    .resizable(true)
123                    .default_width(280.0)
124                    .show(ctx, |ui| {
125                        egui::CollapsingHeader::new("Shape")
126                            .default_open(true)
127                            .show(ui, |ui| {
128                                if ui.add(egui::Slider::new(&mut params.seed, 0.0..=100.0).text("Seed")).changed() { changed = true; should_reset = true; }
129                                if ui.add(egui::Slider::new(&mut params.zoom, 1.0..=10.0).text("Zoom")).changed() { changed = true; should_reset = true; }
130                                if ui.add(egui::Slider::new(&mut params.threshold, 0.3..=0.6).text("Ink Amount")).changed() { changed = true; should_reset = true; }
131                                if ui.add(egui::Slider::new(&mut params.distortion, 0.0..=3.0).text("Warping")).changed() { changed = true; should_reset = true; }
132                                if ui.add(egui::Slider::new(&mut params.fbm_octaves, 1.0..=25.0).text("Detail Octaves")).changed() { 
133                                    params.fbm_octaves = params.fbm_octaves.round();
134                                    changed = true; 
135                                    should_reset = true; 
136                                }
137                            });
138
139                        egui::CollapsingHeader::new("Particle Tracer")
140                            .default_open(false)
141                            .show(ui, |ui| {
142                                changed |= ui.add(egui::Slider::new(&mut params.particle_speed, 0.0..=5.0).text("brush")).changed();
143                                changed |= ui.add(egui::Slider::new(&mut params.trace_steps, 1.0..=100.0).text("Density")).changed();
144                                changed |= ui.add(egui::Slider::new(&mut params.particle_life, 0.8..=0.999).text("Trail Life")).changed();
145                            });
146
147                        egui::CollapsingHeader::new("Visual Settings")
148                            .default_open(false)
149                            .show(ui, |ui| {
150                                ui.label("Primary Ink Color:");
151                                let mut color = [params.color_r, params.color_g, params.color_b];
152                                if ui.color_edit_button_rgb(&mut color).changed() {
153                                    params.color_r = color[0];
154                                    params.color_g = color[1];
155                                    params.color_b = color[2];
156                                    changed = true;
157                                }
158                                changed |= ui.add(egui::Slider::new(&mut params.contrast, 0.5..=6.0).text("Contrast")).changed();
159                                changed |= ui.add(egui::Slider::new(&mut params.gamma, 0.1..=2.0).text("Gamma")).changed();
160                                changed |= ui.add(egui::Slider::new(&mut params.style, 0.0..=1.0).text("Blend")).changed();
161
162                                ui.separator();
163                                ui.horizontal(|ui| {
164                                    ui.label("Phase");
165                                    let mut tint_color = [params.tint_x, params.tint_y, params.tint_z];
166                                    if ui.color_edit_button_rgb(&mut tint_color).changed() {
167                                        params.tint_x = tint_color[0];
168                                        params.tint_y = tint_color[1];
169                                        params.tint_z = tint_color[2];
170                                        changed = true;
171                                    }
172                                });
173                            });
174
175                        ui.separator();
176                        ShaderControls::render_controls_widget(ui, &mut controls_request);
177                        ui.separator();
178                        should_start_export = ExportManager::render_export_ui_widget(ui, &mut export_request);
179                        ui.separator();
180                        
181                        ui.horizontal(|ui| {
182                           ui.label(format!("Frame: {}", self.compute_shader.current_frame));
183                           if ui.button("Clear").clicked() { should_reset = true; }
184                        });
185                    });
186            })
187        } else {
188            self.base.render_ui(core, |_ctx| {})
189        };
190
191        if controls_request.should_clear_buffers || should_reset {
192            self.compute_shader.current_frame = 0;
193            self.compute_shader.time_uniform.data.frame = 0;
194            self.compute_shader.time_uniform.update(&core.queue);
195        }
196
197        self.compute_shader.dispatch(&mut frame.encoder, core);
198
199        self.base.renderer.render_to_view(&mut frame.encoder, &frame.view, &self.compute_shader.get_output_texture().bind_group);
200
201        self.base.apply_control_request(controls_request);
202        self.base.export_manager.apply_ui_request(export_request);
203
204        if changed {
205            self.current_params = params;
206            self.compute_shader.set_custom_params(params, &core.queue);
207        }
208
209        if should_start_export {
210            self.base.export_manager.start_export();
211        }
212
213        self.base.end_frame(core, frame, full_output);
214
215        Ok(())
216    }
217
218    fn handle_input(&mut self, core: &Core, event: &WindowEvent) -> bool {
219        self.base.default_handle_input(core, event)
220    }
221}
222
223fn main() -> Result<(), Box<dyn std::error::Error>> {
224    env_logger::init();
225    let (app, event_loop) = ShaderApp::new("Rorschach Tracer", 700, 500);
226    app.run(event_loop, RorschachShader::init)
227}