From 2cd06c4219225c29f5e90b9a9e732fe827a7c23a 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 | 59 +++++++++++++++++++-------- src/fractal/renderer.rs | 8 +++- src/fractal/share.rs | 5 +++ src/shaders/colorize.wgsl | 80 ++++++++++++++++++++++++++++++------- src/shaders/mandelbrot.wgsl | 9 ++++- 6 files changed, 129 insertions(+), 35 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..3f73244 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,6 +29,8 @@ const INTERACT_DOWNSCALE: u32 = 2; const INTERACT_SETTLE: f64 = 0.12; /// Palette names; index maps to `palette_id` in the shader. const PALETTE_NAMES: &[&str] = &["Amber", "Rainbow", "Ember", "Lime", "Grayscale"]; +/// Shadow palette names; index maps to `palette_id` in the shader. +const SHADOW_PALETTE_NAMES: &[&str] = &["Grayscale", "Red & Blue"]; /// Buddhabrot tonemap style names; index maps to `BuddhabrotUniforms::palette`. const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"]; @@ -121,7 +123,7 @@ const SET_PRESETS: [&[SetPreset]; FractalKind::Lambda as usize + 1] = [ "-0.7436438870371587", "0.1318259042053", 8.0e-8, - 10000, + 2000, None, ), ], @@ -262,11 +264,14 @@ pub struct FractalApp { color_scale: f32, color_offset: f32, palette: u32, + shadow_palette: u32, /// Supersample each pixel 2×2 for smoother edges (costs ~4× fragment work). antialias: bool, /// 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 @@ -401,8 +406,10 @@ impl FractalApp { color_scale: 0.15, color_offset: 0.0, palette: 0, + shadow_palette: 0, antialias: false, de_coloring: false, + shadow: false, buddhabrot: false, buddha_r_cap: 50, buddha_g_cap: 500, @@ -576,6 +583,7 @@ impl FractalApp { color_scale: self.color_scale, color_offset: self.color_offset, palette: self.palette, + shadow_palette: self.shadow_palette, } } @@ -594,6 +602,8 @@ impl FractalApp { self.color_scale = s.color_scale; self.color_offset = s.color_offset; self.palette = (s.palette as usize).min(PALETTE_NAMES.len() - 1) as u32; + self.shadow_palette = + (s.shadow_palette as usize).min(SHADOW_PALETTE_NAMES.len() - 1) as u32; // The link carries an explicit iteration count; honor it rather than // letting the auto-scaler immediately overwrite it. self.auto_iterations = false; @@ -802,14 +812,16 @@ impl FractalApp { bailout_sq: BAILOUT_SQ, is_julia: matches!(self.mode, FractalMode::Julia) as u32, palette_id: self.palette, + shadow_palette_id: self.shadow_palette, aa_level: if self.antialias { 2 } else { 1 }, kind: self.kind as u32, power: self.power, 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, + _pad: [0; _], } } @@ -833,11 +845,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,22 +1366,35 @@ 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); + } + }); + } else { + egui::ComboBox::from_label("palette") + .selected_text(SHADOW_PALETTE_NAMES[self.shadow_palette as usize]) + .show_ui(ui, |ui| { + for (i, name) in SHADOW_PALETTE_NAMES.iter().enumerate() { + ui.selectable_value(&mut self.shadow_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") - .on_hover_text( - "Shade by distance to the set boundary (from the orbit derivative) \ + 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| { ui.checkbox(&mut self.anim.color, "Cycle colours") diff --git a/src/fractal/renderer.rs b/src/fractal/renderer.rs index 33bb037..b96ed9c 100644 --- a/src/fractal/renderer.rs +++ b/src/fractal/renderer.rs @@ -46,6 +46,8 @@ 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_palette_id != b.shadow_palette_id + || a.shadow != b.shadow } /// GPU-side view + coloring parameters. Layout must match `Uniforms` in the @@ -64,12 +66,14 @@ pub struct Uniforms { /// 0 = Mandelbrot, 1 = Julia. pub is_julia: u32, pub palette_id: u32, + pub shadow_palette_id: u32, /// Supersampling factor per axis: 1 = off, 2 = 2×2 (4 samples). pub aa_level: u32, /// Iteration formula (`FractalKind::shader_id`). pub kind: u32, /// Exponent for the Multibrot kind. pub power: u32, + pub _pad: [u32; 1], /// Complex offset of the view center from the reference center, so a stale /// or reused reference (computed at a slightly different center) still maps /// correctly. Added to every pixel's per-pixel offset. @@ -83,8 +87,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/fractal/share.rs b/src/fractal/share.rs index 4e06d9d..4550b29 100644 --- a/src/fractal/share.rs +++ b/src/fractal/share.rs @@ -29,6 +29,8 @@ pub struct ShareState { pub color_offset: f32, /// Palette index (`palette_id` in the shader). pub palette: u32, + /// Shadow palette index (`shadow_palette_id` in the shader). + pub shadow_palette: u32, } impl ShareState { @@ -110,6 +112,7 @@ impl ShareState { color_scale: map.get("cs").and_then(|s| s.parse().ok()).unwrap_or(0.02), color_offset: map.get("co").and_then(|s| s.parse().ok()).unwrap_or(0.0), palette: map.get("pal").and_then(|s| s.parse().ok()).unwrap_or(0), + shadow_palette: map.get("spal").and_then(|s| s.parse().ok()).unwrap_or(0), }) } } @@ -134,6 +137,7 @@ mod tests { color_scale: 0.02, color_offset: 0.25, palette: 3, + shadow_palette: 1, }; let d = ShareState::decode(&s.encode()).unwrap(); assert_eq!(d.julia, s.julia); @@ -146,6 +150,7 @@ mod tests { assert_eq!(d.julia_c, s.julia_c); assert_eq!(d.phoenix_p, s.phoenix_p); assert_eq!(d.palette, s.palette); + assert_eq!(d.shadow_palette, s.shadow_palette); } #[test] diff --git a/src/shaders/colorize.wgsl b/src/shaders/colorize.wgsl index 299f1f8..d18b216 100644 --- a/src/shaders/colorize.wgsl +++ b/src/shaders/colorize.wgsl @@ -20,12 +20,15 @@ struct Uniforms { bailout_sq: f32, is_julia: u32, palette_id: u32, + shadow_palette_id: u32, aa_level: u32, kind: u32, 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 +37,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 +65,66 @@ 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); +} + +fn compute_light(normal: vec3, light: vec3) -> vec3 { + return vec3((dot(normal, normalize(light)) + 0.2) / 1.2); +} + +fn uncharted2tonemap(x: vec3) -> vec3 { +// Paramètres de la courbe de Hable + let A = 0.15; // Shoulder strength + let B = 0.50; // Linear strength + let C = 0.10; // Linear angle + let D = 0.20; // Toe strength + let E = 0.02; // Toe numerator / shoarder angle/etc. + let F = 0.30; // Toe denominator + + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; +}fn filmic(color: vec3) -> vec3 { + let exposure_bias = 2.0; + let curr = uncharted2tonemap(color * exposure_bias); + + // Valeur blanche maximale de référence + let white_scale = vec3(1.0) / uncharted2tonemap(vec3(4.2)); + return curr * white_scale; +} + @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])); + + var color: vec3; + if u.shadow_palette_id == 0u { + color = compute_light(normal,vec3(.5, .5, .5)); + } else { + color = compute_light(normal, vec3(0., .5, .5)) * vec3(1., 0.5, 0.5) + compute_light(normal, vec3(0.5, 0., .5)) * vec3(0.5, 1., 1.); + + color = filmic(color); + } + + return vec4(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..224ee21 100644 --- a/src/shaders/mandelbrot.wgsl +++ b/src/shaders/mandelbrot.wgsl @@ -21,6 +21,7 @@ struct Uniforms { bailout_sq: f32, is_julia: u32, palette_id: u32, + shadow_palette_id: u32, aa_level: u32, // Iteration formula (see the KIND_* constants below). kind: u32, @@ -35,6 +36,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 +329,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); }