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}