From e05c663e400353c47ad873dd4928af5c22de8d7b Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Fri, 18 Sep 2026 10:12:41 +0200 Subject: [PATCH] feat: add shadow coloring --- Cargo.toml | 3 ++- src/app.rs | 30 ++++++++++++++--------- src/fractal/renderer.rs | 5 ++-- src/shaders/colorize.wgsl | 49 ++++++++++++++++++++++++++----------- src/shaders/mandelbrot.wgsl | 8 +++++- 5 files changed, 65 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 65d3e9a..ae9329b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ web-sys = { version = "0.3.105", features = ["Window", "Location", "Url", "UrlSe # Release: optimize hard (fractal math is hot). [profile.release] opt-level = 3 -codegen-units = 1 +# codegen-units = 1 +debug = true # Dev: keep our own crate debuggable, but optimize dependencies (dashu, wgpu, # egui) so the explorer is actually interactive during development. diff --git a/src/app.rs b/src/app.rs index 1f79111..93bf2dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -267,6 +267,8 @@ pub struct FractalApp { /// Distance-estimation shading: darkens toward the set boundary using the /// orbit derivative, giving crisp filaments at deep zoom instead of speckle. de_coloring: bool, + // Use shadow coloring + shadow: bool, /// Render as a Buddhabrot (Monte-Carlo orbit-density histogram) instead of /// the ordinary escape-time set. Plain f32 view — no deep zoom, no @@ -403,6 +405,7 @@ impl FractalApp { palette: 0, antialias: false, de_coloring: false, + shadow: false, buddhabrot: false, buddha_r_cap: 50, buddha_g_cap: 500, @@ -808,8 +811,8 @@ impl FractalApp { dc_offset: self.dc_offset(), 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], - de_coloring: self.de_coloring as u32, - _pad: [0], + de_coloring: (self.de_coloring | self.shadow) as u32, + shadow: self.shadow as u32, } } @@ -833,11 +836,11 @@ impl FractalApp { r_cap: self.buddha_r_cap, g_cap: self.buddha_g_cap, b_cap: self.buddha_b_cap, - seed: 0, // set by the callback's own dispatch counter + seed: 0, // set by the callback's own dispatch counter samples_this_dispatch: 0, // set by the callback exposure: self.buddha_exposure, - width: 0, // set by the callback from size_px - height: 0, // set by the callback from size_px + width: 0, // set by the callback from size_px + height: 0, // set by the callback from size_px total_samples: 0.0, // tracked by the renderer across frames palette: self.buddha_palette, _pad: [0; 3], @@ -1354,13 +1357,16 @@ impl FractalApp { .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| { - for (i, name) in PALETTE_NAMES.iter().enumerate() { - ui.selectable_value(&mut self.palette, i as u32, *name); - } - }); + ui.checkbox(&mut self.shadow, "Shadow"); + if !self.shadow { + egui::ComboBox::from_label("palette") + .selected_text(PALETTE_NAMES[self.palette as usize]) + .show_ui(ui, |ui| { + for (i, name) in PALETTE_NAMES.iter().enumerate() { + ui.selectable_value(&mut self.palette, i as u32, *name); + } + }); + } ui.checkbox(&mut self.antialias, "Antialiasing (2×2)") .on_hover_text("Supersample each pixel for smoother edges (~4× slower)."); ui.checkbox(&mut self.de_coloring, "Distance shading") diff --git a/src/fractal/renderer.rs b/src/fractal/renderer.rs index 33bb037..6a7e2dc 100644 --- a/src/fractal/renderer.rs +++ b/src/fractal/renderer.rs @@ -46,6 +46,7 @@ fn color_differs(a: &Uniforms, b: &Uniforms) -> bool { a.color_offset != b.color_offset || a.color_scale != b.color_scale || a.palette_id != b.palette_id + || a.shadow != b.shadow } /// GPU-side view + coloring parameters. Layout must match `Uniforms` in the @@ -83,8 +84,8 @@ pub struct Uniforms { pub lambda_l: [f32; 2], /// 0 = escape-time coloring, 1 = distance-estimation shading. pub de_coloring: u32, - /// Padding to a 16-byte multiple (uniform buffer requirement). - pub _pad: [u32; 1], + // 0 = classic colors, 1 = shadows + pub shadow: u32, } /// Offscreen textures for the two-pass render, recreated whenever the widget's diff --git a/src/shaders/colorize.wgsl b/src/shaders/colorize.wgsl index 299f1f8..1cef782 100644 --- a/src/shaders/colorize.wgsl +++ b/src/shaders/colorize.wgsl @@ -25,7 +25,9 @@ struct Uniforms { power: u32, dc_offset: vec2, phoenix_p: vec2, + lambda_l: vec2, de_coloring: u32, + shadow: u32, }; @group(0) @binding(0) var u: Uniforms; @@ -34,18 +36,18 @@ struct Uniforms { // Smooth cyclic palettes (Inigo Quilez cosine palettes). Must match the palette // in mandelbrot.wgsl. fn palette(id: u32, t: f32) -> vec3 { - if (id == 4u) { + if id == 4u { return vec3(t, t, t); // grayscale } let a = vec3(0.5, 0.5, 0.5); let b = vec3(0.5, 0.5, 0.5); var c = vec3(1.0, 1.0, 1.0); var d = vec3(0.00, 0.10, 0.20); // 0: amber / blue - if (id == 1u) { + if id == 1u { d = vec3(0.00, 0.33, 0.67); // rainbow - } else if (id == 2u) { + } else if id == 2u { d = vec3(0.30, 0.20, 0.20); // warm ember - } else if (id == 3u) { + } else if id == 3u { c = vec3(1.0, 1.0, 0.5); d = vec3(0.80, 0.90, 0.30); // lime / magenta } @@ -62,17 +64,36 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4 { return vec4(verts[idx], 0.0, 1.0); } +fn load(x: i32, y: i32) -> vec3 { + let dist = textureLoad(data_tex, vec2(x, y), 0).g; + return vec3(f32(x), f32(y), dist); +} + @fragment fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { - 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; + if u.shadow != 0u { + if textureLoad(data_tex, vec2(i32(pos.x), i32(pos.y)), 0).b != 0. { + return vec4(0., 0.11, 0.54, 1.0); + } else { + let d = array, 3>(load(i32(pos.x), i32(pos.y)), load(i32(pos.x + 1), i32(pos.y)), load(i32(pos.x), i32(pos.y + 1))); - let t = fract(ci * u.color_scale + u.color_offset); - var col = palette(u.palette_id, t) * 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); + let normal = normalize(cross(d[1] - d[0], d[2] - d[0])); + + let color = (dot(normal, vec3(.5, .5, .5)) + 0.2) / 1.2; + + return vec4(vec3(color), 1.0); + } + } 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; + + let t = fract(ci * u.color_scale + u.color_offset); + var col = palette(u.palette_id, t) * 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/mandelbrot.wgsl b/src/shaders/mandelbrot.wgsl index 4e99935..a9655ad 100644 --- a/src/shaders/mandelbrot.wgsl +++ b/src/shaders/mandelbrot.wgsl @@ -35,6 +35,8 @@ struct Uniforms { lambda_l: vec2, // 0 = escape-time coloring, 1 = distance-estimation shading. de_coloring: u32, + // 0 = classic colors, 1 = shadows + shadow: u32, }; const KIND_MANDELBROT: u32 = 0u; @@ -326,7 +328,11 @@ fn iterate_sample(offset: vec2, px: f32) -> Sample { let zmag = sqrt(max(z2, 1.0)); let dzmag = sqrt(max(dot(dz, dz), 1e-20)); let d = zmag * log(zmag) / dzmag; - de = clamp(d / max(px, 1e-30), 0.0, 1.0); + var max_de = 1.; + if u.shadow != 0u { + max_de = 1000.; + } + de = clamp(d / max(px, 1e-30), 0.0, max_de); } return Sample(ci, de, true); }