diff --git a/src/app.rs b/src/app.rs index e2fe143..9dfef62 100644 --- a/src/app.rs +++ b/src/app.rs @@ -261,9 +261,8 @@ pub struct FractalApp { /// orbit derivative, giving crisp filaments at deep zoom instead of speckle. de_coloring: bool, // Use shadow coloring - shadow: bool, // Use 3D raymarching rendering - dimension3: bool, + rendering_mode: u32, /// List of enabled lights in the world lights: Vec, @@ -435,8 +434,7 @@ impl FractalApp { shadow_palette: 0, antialias: false, de_coloring: false, - shadow: false, - dimension3: false, + rendering_mode: 0, lights: vec![Light::default()], buddha_r_cap: 50, buddha_g_cap: 500, @@ -937,14 +935,12 @@ impl FractalApp { phoenix_p: [self.phoenix_p.0 as f32, self.phoenix_p.1 as f32], lambda_l: [self.lambda_l.0 as f32, self.lambda_l.1 as f32], complex_power: [self.complex_power.0 as f32, self.complex_power.1 as f32], - de_coloring: (self.de_coloring | self.shadow | self.dimension3) as u32, - shadow: self.shadow as u32, - dimension3: self.dimension3 as u32, + de_coloring: (self.de_coloring | (self.rendering_mode > 0)) as u32, + rendering_mode: self.rendering_mode, camera_direction: self.camera.direction().to_array(), + camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(), _pad: [0; _], _pad2: [0; _], - _pad3: [0; _], - camera_inv_proj: self.camera.orthographic().inverse().to_cols_array(), } } @@ -1632,6 +1628,13 @@ impl FractalApp { }); } + ui.label("rendering:"); + ui.horizontal(|ui| { + ui.radio_value(&mut self.rendering_mode, 0, "Classic"); + ui.radio_value(&mut self.rendering_mode, 1, "Shadow"); + ui.radio_value(&mut self.rendering_mode, 2, "3D"); + }); + ui.add_space(4.); ui.separator(); ui.add_space(4.); @@ -1646,15 +1649,29 @@ impl FractalApp { .logarithmic(true), ); } - ui.add( - egui::Slider::new(&mut self.color_scale, 0.01..=1.0) - .text("color scale") - .logarithmic(true), - ); - ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset")); - ui.checkbox(&mut self.shadow, "Shadow rendering"); - ui.checkbox(&mut self.dimension3, "3D rendering"); - if !self.shadow { + ui.checkbox(&mut self.antialias, "Antialiasing (2×2)") + .on_hover_text("Supersample each pixel for smoother edges (~4× slower)."); + if self.rendering_mode == 0 { + ui.checkbox(&mut self.de_coloring, "Distance shading") + .on_hover_text( + "Shade by distance to the set boundary (from the orbit derivative) \ + for crisp filaments at deep zoom. Exact for the holomorphic kinds \ + (Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \ + (Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).", + ); + } + + ui.add_space(4.); + ui.separator(); + ui.add_space(4.); + + if self.rendering_mode == 0 { + ui.add( + egui::Slider::new(&mut self.color_scale, 0.01..=1.0) + .text("color scale") + .logarithmic(true), + ); + ui.add(egui::Slider::new(&mut self.color_offset, 0.0..=1.0).text("color offset")); egui::ComboBox::from_label("palette") .selected_text(PALETTE_NAMES[self.palette as usize]) .show_ui(ui, |ui| { @@ -1671,7 +1688,8 @@ impl FractalApp { } }); } - if self.shadow && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1 { + if self.rendering_mode > 0 && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1 + { ui.horizontal(|ui| { ui.label("lights:"); if ui.button("+").clicked() { @@ -1692,20 +1710,9 @@ impl FractalApp { ui.add_space(4.); ui.separator(); ui.add_space(4.); - ui.checkbox(&mut self.antialias, "Antialiasing (2×2)") - .on_hover_text("Supersample each pixel for smoother edges (~4× slower)."); - if !self.shadow { - ui.checkbox(&mut self.de_coloring, "Distance shading") - .on_hover_text( - "Shade by distance to the set boundary (from the orbit derivative) \ - for crisp filaments at deep zoom. Exact for the holomorphic kinds \ - (Mandelbrot/Multibrot/Phoenix), approximate for the abs-based kinds \ - (Burning Ship/Tricorn/Celtic/Perpendicular/Buffalo).", - ); - } ui.collapsing("Animation", |ui| { - if !self.shadow { + if self.rendering_mode == 0 { ui.checkbox(&mut self.anim.color, "Cycle colours") .on_hover_text("Scroll the palette offset over time."); if self.anim.color { @@ -1982,7 +1989,7 @@ impl FractalApp { // instead of panning/zooming the 2D fractal view. const ROT_SENS: f32 = 0.002; // radians per dragged pixel let multi_touch = ui.input(|i| i.multi_touch()); - if self.dimension3 { + if self.rendering_mode == 3 { if let Some(mt) = multi_touch { let t = mt.translation_delta; if t.x != 0.0 || t.y != 0.0 { @@ -2033,7 +2040,7 @@ impl FractalApp { && rect.contains(pos) { let factor = (-scroll_y as f64 * 0.0015).exp(); - if self.dimension3 { + if self.rendering_mode == 3 { self.camera.zoom(factor as f32); } else { let off = pos - rect.center(); @@ -2053,7 +2060,7 @@ impl FractalApp { if !ui.ctx().egui_wants_keyboard_input() { let dt = ui.input(|i| i.stable_dt as f64).clamp(0.0, 0.1); - if self.dimension3 { + if self.rendering_mode == 3 { let (fwd, back, strafe_l, strafe_r, up, down, look_l, look_r, look_u, look_d) = ui .input(|i| { ( diff --git a/src/fractal/renderer.rs b/src/fractal/renderer.rs index 286cdd3..2fcac4a 100644 --- a/src/fractal/renderer.rs +++ b/src/fractal/renderer.rs @@ -50,7 +50,7 @@ fn color_differs(a: &Uniforms, b: &Uniforms) -> bool { || a.color_scale != b.color_scale || a.palette_id != b.palette_id || a.shadow_palette_id != b.shadow_palette_id - || a.shadow != b.shadow + || a.rendering_mode != b.rendering_mode } /// GPU-side view + coloring parameters. Layout must match `Uniforms` in the @@ -93,14 +93,11 @@ pub struct Uniforms { pub complex_power: [f32; 2], /// 0 = escape-time coloring, 1 = distance-estimation shading. pub de_coloring: u32, - // 0 = classic colors, 1 = shadows - pub shadow: u32, - // 0 = classic colors, 1 = 3D raymarching rendering - pub dimension3: u32, - pub _pad2: [u32; 3], + // 0 = classic colors, 1 = shadows, 2 = 3D raymarching rendering + pub rendering_mode: u32, // camera direction vector pub camera_direction: [f32; 3], - pub _pad3: [u32; 1], + pub _pad2: [u32; 1], /// Inverse of the camera's view-projection matrix (column-major), for /// reconstructing a world-space ray origin per pixel in the raymarcher. pub camera_inv_proj: [f32; 16], diff --git a/src/lights.rs b/src/lights.rs index aefe241..e45d331 100644 --- a/src/lights.rs +++ b/src/lights.rs @@ -1,7 +1,7 @@ use std::f32::consts::PI; use bytemuck::{Pod, Zeroable}; -use egui::{Color32, Ui}; +use egui::{Button, Color32, Ui}; /// Maximum number of simultaneous lights. pub const MAX_LIGHT_COUNT: usize = 16; @@ -28,7 +28,12 @@ impl Default for Light { impl Light { pub fn widget(&mut self, ui: &mut Ui) -> bool { - let formater = |v, _| format!("{}°", ((v as f32 * 180. / PI) as u32)); + let formater = |v, _| format!("{}°", ((v * 180. / std::f64::consts::PI) as u32)); + let parser = |s: &str| { + s.parse::() + .ok() + .map(|x| x as f64 * std::f64::consts::PI / 180.) + }; ui.horizontal(|ui| { let del = ui.button("-").clicked(); ui.label("color:"); @@ -38,6 +43,7 @@ impl Light { egui::DragValue::new(&mut self.azimuth) .range(0.0..=PI * 2.) .custom_formatter(formater) + .custom_parser(parser) .speed(0.02), ); ui.label("φ:"); @@ -45,6 +51,7 @@ impl Light { egui::DragValue::new(&mut self.altitude) .range(0.0..=PI / 2.) .custom_formatter(formater) + .custom_parser(parser) .speed(0.02), ); diff --git a/src/shaders/colorize.wgsl b/src/shaders/colorize.wgsl index 562f95f..826bafc 100644 --- a/src/shaders/colorize.wgsl +++ b/src/shaders/colorize.wgsl @@ -35,23 +35,21 @@ fn shadow_fragment(pos: vec3) -> vec4 { } @fragment fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { - if u.dimension3 == 0u { - if u.shadow != 0u { - return shadow_fragment(pos.xyz); - } else { - let d = textureLoad(data_tex, vec2(i32(pos.x), i32(pos.y)), 0); - let ci = d.r; - let de = d.g; - let interior_frac = d.b; - - var col = classic_color(ci, de); - // Anti-alias the set boundary: fade toward black by the fraction of the - // pixel's sub-samples that landed in the interior. - col = col * (1.0 - interior_frac); - return vec4(col, 1.0); - } - } else { + if u.shadow == 2u { return ray_marching(pos); + } else if u.shadow == 1u { + return shadow_fragment(pos.xyz); + } else { + let d = textureLoad(data_tex, vec2(i32(pos.x), i32(pos.y)), 0); + let ci = d.r; + let de = d.g; + let interior_frac = d.b; + + var col = classic_color(ci, de); + // Anti-alias the set boundary: fade toward black by the fraction of the + // pixel's sub-samples that landed in the interior. + col = col * (1.0 - interior_frac); + return vec4(col, 1.0); } } diff --git a/src/shaders/iterate_uniforms.wgsl b/src/shaders/iterate_uniforms.wgsl index 249d276..dcdab06 100644 --- a/src/shaders/iterate_uniforms.wgsl +++ b/src/shaders/iterate_uniforms.wgsl @@ -31,10 +31,8 @@ struct Uniforms { complex_power: vec2, // 0 = escape-time coloring, 1 = distance-estimation shading. de_coloring: u32, - // 0 = classic colors, 1 = shadows + // 0 = classic colors, 1 = shadows, 2 = 3D raymarching rendering shadow: u32, - // Use 3D raymarching rendering - dimension3: u32, // camera direction vector camera_direction: vec3, // inverse of the camera's view-projection matrix, for reconstructing a