From 9cc5a80650d140271de5b5d4c1fd4d9c38ec3672 Mon Sep 17 00:00:00 2001 From: supersurviveur Date: Thu, 24 Sep 2026 21:39:00 +0200 Subject: [PATCH] feat: add classic colors in 3d --- src/app.rs | 40 ++++++++++++++++++++----------- src/shaders/colorize.wgsl | 4 ++-- src/shaders/iterate_uniforms.wgsl | 23 ++++++++++++++++-- src/shaders/mandelbrot.wgsl | 4 ++-- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9fa9a3e..ce125d3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -39,8 +39,13 @@ 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", "Custom lights"]; +/// Shadow palette names; index maps to `shadow_palette_id` in the shader. +/// Append new entries: share links store the index. +const SHADOW_PALETTE_NAMES: &[&str] = &["Grayscale", "Red & Blue", "Custom lights", "Classic"]; +/// Shadow palette lit by the user's `lights` list. +const SHADOW_PALETTE_CUSTOM_LIGHTS: u32 = 2; +/// Shadow palette that paints the classic escape-time palette, lit. +const SHADOW_PALETTE_CLASSIC: u32 = 3; /// Buddhabrot tonemap style names; index maps to `BuddhabrotUniforms::palette`. const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"]; @@ -992,6 +997,13 @@ impl FractalApp { self.last_request = Some(key); } + /// Whether the classic escape-time palette (and its scale / offset / + /// palette controls) is in use: always in classic mode, and in shadow/3D + /// modes under the "Classic" shading palette. + fn uses_classic_palette(&self) -> bool { + self.rendering_mode == 0 || self.shadow_palette == SHADOW_PALETTE_CLASSIC + } + /// The rendering mode the shaders should use this frame: 3D for as long /// as the 2D <-> 3D camera transition is in flight (the raymarcher, and /// the DE heights it reads, stay on until the camera is back top-down), @@ -1801,7 +1813,16 @@ impl FractalApp { ui.separator(); ui.add_space(4.); - if self.rendering_mode == 0 { + if self.rendering_mode != 0 { + egui::ComboBox::from_label("shading") + .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); + } + }); + } + if self.uses_classic_palette() { ui.add( egui::Slider::new(&mut self.color_scale, 0.01..=1.0) .text("color scale") @@ -1815,17 +1836,8 @@ impl FractalApp { 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); - } - }); } - if self.rendering_mode > 0 && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1 - { + if self.rendering_mode != 0 && self.shadow_palette == SHADOW_PALETTE_CUSTOM_LIGHTS { ui.horizontal(|ui| { ui.label("lights:"); if ui.button("+").clicked() { @@ -1848,7 +1860,7 @@ impl FractalApp { ui.add_space(4.); ui.collapsing("Animation", |ui| { - if self.rendering_mode == 0 { + if self.uses_classic_palette() { ui.checkbox(&mut self.anim.color, "Cycle colours") .on_hover_text("Scroll the palette offset over time."); if self.anim.color { diff --git a/src/shaders/colorize.wgsl b/src/shaders/colorize.wgsl index 327e52e..323d0b9 100644 --- a/src/shaders/colorize.wgsl +++ b/src/shaders/colorize.wgsl @@ -25,7 +25,7 @@ fn shadow_fragment(pos: vec2) -> vec4 { let size = textureDimensions(data_tex); let here = textureLoad(data_tex, vec2(x, y), 0); if here.b != 0. { - return vec4(0.1, 0.1, 0.1, 1.0); + return vec4(shadow_interior_color(), 1.0); } // Forward differences, except on the last column/row where x+1 / y+1 // is off the texture: fall back to a backward difference, mirrored @@ -45,7 +45,7 @@ fn shadow_fragment(pos: vec2) -> vec4 { h2 = 2.0 * h0 - textureLoad(data_tex, vec2(x, y - 1), 0).g; } let normal = normal_from_heights(h0, h1, h2); - return vec4(shadow_color(normal), 1.0); + return vec4(shadow_color(normal, here.r), 1.0); } @fragment diff --git a/src/shaders/iterate_uniforms.wgsl b/src/shaders/iterate_uniforms.wgsl index 1420b3d..9f809c5 100644 --- a/src/shaders/iterate_uniforms.wgsl +++ b/src/shaders/iterate_uniforms.wgsl @@ -141,10 +141,12 @@ fn normal_from_heights(h0: f32, h1: f32, h2: f32) -> vec3 { } // Shade a DE-derived surface normal per `u.shadow_palette_id`: 0 = grayscale -// key light, 1 = red/blue two-tone, 2 = the user's custom `lights` list. +// key light, 1 = red/blue two-tone, 2 = the user's custom `lights` list, +// 3 = the classic escape-time palette at `ci` (the smoothed iteration count), +// lit by the grayscale key light. // Shared by the interactive shadow pass (colorize.wgsl) and the PNG-export // shadow path (mandelbrot.wgsl's `fs_color`), which must render identically. -fn shadow_color(normal: vec3) -> vec3 { +fn shadow_color(normal: vec3, ci: f32) -> vec3 { var color: vec3; if u.shadow_palette_id == 0u { color = compute_light(normal, vec3(0.57735027, 0.57735027, 0.57735027)) + vec3(0.58, 0.85, 1.) * 0.2; @@ -155,6 +157,14 @@ fn shadow_color(normal: vec3) -> vec3 { color = compute_light(normal, vec3(0., 0.70710678, 0.70710678)) * vec3(1., 0.5, 0.5) + compute_light(normal, vec3(0.70710678, 0., 0.70710678)) * vec3(0.5, 1., 1.); color = filmic(color, 4.2); + } else if u.shadow_palette_id == 3u { + // No DE darkening as in `classic_color`: in shadow/3D modes the DE + // is a height (clamped to 1000, not 1), and the lighting already + // shows the relief. + let t = fract(ci * u.color_scale + u.color_offset); + let ambient = 0.25; + let light = compute_light(normal, vec3(0.57735027, 0.57735027, 0.57735027)); + color = palette(u.palette_id, t) * (ambient + (1.0 - ambient) * light); } else { color = vec3(0); let light_count = min(u.light_count, 16u); @@ -167,3 +177,12 @@ fn shadow_color(normal: vec3) -> vec3 { } return color; } + +// Colour of an interior (non-escaped) pixel in shadow/3D modes: black under +// the classic palette, like classic 2D mode, otherwise a dark gray plateau. +fn shadow_interior_color() -> vec3 { + if u.shadow_palette_id == 3u { + return vec3(0.0); + } + return vec3(0.1); +} diff --git a/src/shaders/mandelbrot.wgsl b/src/shaders/mandelbrot.wgsl index 76a163f..a1537df 100644 --- a/src/shaders/mandelbrot.wgsl +++ b/src/shaders/mandelbrot.wgsl @@ -595,12 +595,12 @@ fn fs_color(in: VsOut) -> @location(0) vec4 { // neighbours a `dx`/`dy` step away. let here = aggregate_sample(base, dx, dy, px, aa); if here.z != 0.0 { - return vec4(0.1, 0.1, 0.1, 1.0); + return vec4(shadow_interior_color(), 1.0); } let right = aggregate_sample(base + dx, dx, dy, px, aa); let down = aggregate_sample(base + dy, dx, dy, px, aa); let normal = normal_from_heights(here.y, right.y, down.y); - return vec4(shadow_color(normal), 1.0); + return vec4(shadow_color(normal, here.x), 1.0); } let inv = 1.0 / f32(aa);