feat: add classic colors in 3d

This commit is contained in:
2026-09-24 21:39:00 +02:00
parent 6a6796f528
commit 9cc5a80650
4 changed files with 51 additions and 20 deletions
+26 -14
View File
@@ -39,8 +39,13 @@ const INTERACT_DOWNSCALE: u32 = 2;
const INTERACT_SETTLE: f64 = 0.12; const INTERACT_SETTLE: f64 = 0.12;
/// Palette names; index maps to `palette_id` in the shader. /// Palette names; index maps to `palette_id` in the shader.
const PALETTE_NAMES: &[&str] = &["Amber", "Rainbow", "Ember", "Lime", "Grayscale"]; const PALETTE_NAMES: &[&str] = &["Amber", "Rainbow", "Ember", "Lime", "Grayscale"];
/// Shadow palette names; index maps to `palette_id` in the shader. /// Shadow palette names; index maps to `shadow_palette_id` in the shader.
const SHADOW_PALETTE_NAMES: &[&str] = &["Grayscale", "Red & Blue", "Custom lights"]; /// 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`. /// Buddhabrot tonemap style names; index maps to `BuddhabrotUniforms::palette`.
const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"]; const BUDDHA_PALETTE_NAMES: &[&str] = &["Nebula", "Yellow", "Grayscale"];
@@ -992,6 +997,13 @@ impl FractalApp {
self.last_request = Some(key); 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 /// 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 /// 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), /// the DE heights it reads, stay on until the camera is back top-down),
@@ -1801,7 +1813,16 @@ impl FractalApp {
ui.separator(); ui.separator();
ui.add_space(4.); 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( ui.add(
egui::Slider::new(&mut self.color_scale, 0.01..=1.0) egui::Slider::new(&mut self.color_scale, 0.01..=1.0)
.text("color scale") .text("color scale")
@@ -1815,17 +1836,8 @@ impl FractalApp {
ui.selectable_value(&mut self.palette, i as u32, *name); 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 == SHADOW_PALETTE_CUSTOM_LIGHTS {
}
if self.rendering_mode > 0 && self.shadow_palette as usize == SHADOW_PALETTE_NAMES.len() - 1
{
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label("lights:"); ui.label("lights:");
if ui.button("+").clicked() { if ui.button("+").clicked() {
@@ -1848,7 +1860,7 @@ impl FractalApp {
ui.add_space(4.); ui.add_space(4.);
ui.collapsing("Animation", |ui| { ui.collapsing("Animation", |ui| {
if self.rendering_mode == 0 { if self.uses_classic_palette() {
ui.checkbox(&mut self.anim.color, "Cycle colours") ui.checkbox(&mut self.anim.color, "Cycle colours")
.on_hover_text("Scroll the palette offset over time."); .on_hover_text("Scroll the palette offset over time.");
if self.anim.color { if self.anim.color {
+2 -2
View File
@@ -25,7 +25,7 @@ fn shadow_fragment(pos: vec2<f32>) -> vec4<f32> {
let size = textureDimensions(data_tex); let size = textureDimensions(data_tex);
let here = textureLoad(data_tex, vec2<i32>(x, y), 0); let here = textureLoad(data_tex, vec2<i32>(x, y), 0);
if here.b != 0. { if here.b != 0. {
return vec4<f32>(0.1, 0.1, 0.1, 1.0); return vec4<f32>(shadow_interior_color(), 1.0);
} }
// Forward differences, except on the last column/row where x+1 / y+1 // Forward differences, except on the last column/row where x+1 / y+1
// is off the texture: fall back to a backward difference, mirrored // is off the texture: fall back to a backward difference, mirrored
@@ -45,7 +45,7 @@ fn shadow_fragment(pos: vec2<f32>) -> vec4<f32> {
h2 = 2.0 * h0 - textureLoad(data_tex, vec2<i32>(x, y - 1), 0).g; h2 = 2.0 * h0 - textureLoad(data_tex, vec2<i32>(x, y - 1), 0).g;
} }
let normal = normal_from_heights(h0, h1, h2); let normal = normal_from_heights(h0, h1, h2);
return vec4<f32>(shadow_color(normal), 1.0); return vec4<f32>(shadow_color(normal, here.r), 1.0);
} }
@fragment @fragment
+21 -2
View File
@@ -141,10 +141,12 @@ fn normal_from_heights(h0: f32, h1: f32, h2: f32) -> vec3<f32> {
} }
// Shade a DE-derived surface normal per `u.shadow_palette_id`: 0 = grayscale // 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 // Shared by the interactive shadow pass (colorize.wgsl) and the PNG-export
// shadow path (mandelbrot.wgsl's `fs_color`), which must render identically. // shadow path (mandelbrot.wgsl's `fs_color`), which must render identically.
fn shadow_color(normal: vec3<f32>) -> vec3<f32> { fn shadow_color(normal: vec3<f32>, ci: f32) -> vec3<f32> {
var color: vec3<f32>; var color: vec3<f32>;
if u.shadow_palette_id == 0u { if u.shadow_palette_id == 0u {
color = compute_light(normal, vec3<f32>(0.57735027, 0.57735027, 0.57735027)) + vec3<f32>(0.58, 0.85, 1.) * 0.2; color = compute_light(normal, vec3<f32>(0.57735027, 0.57735027, 0.57735027)) + vec3<f32>(0.58, 0.85, 1.) * 0.2;
@@ -155,6 +157,14 @@ fn shadow_color(normal: vec3<f32>) -> vec3<f32> {
color = compute_light(normal, vec3<f32>(0., 0.70710678, 0.70710678)) * vec3<f32>(1., 0.5, 0.5) + compute_light(normal, vec3<f32>(0.70710678, 0., 0.70710678)) * vec3<f32>(0.5, 1., 1.); color = compute_light(normal, vec3<f32>(0., 0.70710678, 0.70710678)) * vec3<f32>(1., 0.5, 0.5) + compute_light(normal, vec3<f32>(0.70710678, 0., 0.70710678)) * vec3<f32>(0.5, 1., 1.);
color = filmic(color, 4.2); 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<f32>(0.57735027, 0.57735027, 0.57735027));
color = palette(u.palette_id, t) * (ambient + (1.0 - ambient) * light);
} else { } else {
color = vec3<f32>(0); color = vec3<f32>(0);
let light_count = min(u.light_count, 16u); let light_count = min(u.light_count, 16u);
@@ -167,3 +177,12 @@ fn shadow_color(normal: vec3<f32>) -> vec3<f32> {
} }
return color; 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<f32> {
if u.shadow_palette_id == 3u {
return vec3<f32>(0.0);
}
return vec3<f32>(0.1);
}
+2 -2
View File
@@ -595,12 +595,12 @@ fn fs_color(in: VsOut) -> @location(0) vec4<f32> {
// neighbours a `dx`/`dy` step away. // neighbours a `dx`/`dy` step away.
let here = aggregate_sample(base, dx, dy, px, aa); let here = aggregate_sample(base, dx, dy, px, aa);
if here.z != 0.0 { if here.z != 0.0 {
return vec4<f32>(0.1, 0.1, 0.1, 1.0); return vec4<f32>(shadow_interior_color(), 1.0);
} }
let right = aggregate_sample(base + dx, dx, dy, px, aa); let right = aggregate_sample(base + dx, dx, dy, px, aa);
let down = aggregate_sample(base + dy, 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); let normal = normal_from_heights(here.y, right.y, down.y);
return vec4<f32>(shadow_color(normal), 1.0); return vec4<f32>(shadow_color(normal, here.x), 1.0);
} }
let inv = 1.0 / f32(aa); let inv = 1.0 / f32(aa);