feat: add shadow coloring
This commit is contained in:
+2
-1
@@ -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.
|
||||
|
||||
+42
-17
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
+66
-14
@@ -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<f32>,
|
||||
phoenix_p: vec2<f32>,
|
||||
lambda_l: vec2<f32>,
|
||||
de_coloring: u32,
|
||||
shadow: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> 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<f32> {
|
||||
if (id == 4u) {
|
||||
if id == 4u {
|
||||
return vec3<f32>(t, t, t); // grayscale
|
||||
}
|
||||
let a = vec3<f32>(0.5, 0.5, 0.5);
|
||||
let b = vec3<f32>(0.5, 0.5, 0.5);
|
||||
var c = vec3<f32>(1.0, 1.0, 1.0);
|
||||
var d = vec3<f32>(0.00, 0.10, 0.20); // 0: amber / blue
|
||||
if (id == 1u) {
|
||||
if id == 1u {
|
||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
||||
} else if (id == 2u) {
|
||||
} else if id == 2u {
|
||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
||||
} else if (id == 3u) {
|
||||
} else if id == 3u {
|
||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
||||
}
|
||||
@@ -62,17 +65,66 @@ fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
||||
return vec4<f32>(verts[idx], 0.0, 1.0);
|
||||
}
|
||||
|
||||
fn load(x: i32, y: i32) -> vec3<f32> {
|
||||
let dist = textureLoad(data_tex, vec2<i32>(x, y), 0).g;
|
||||
return vec3<f32>(f32(x), f32(y), dist);
|
||||
}
|
||||
|
||||
fn compute_light(normal: vec3<f32>, light: vec3<f32>) -> vec3<f32> {
|
||||
return vec3<f32>((dot(normal, normalize(light)) + 0.2) / 1.2);
|
||||
}
|
||||
|
||||
fn uncharted2tonemap(x: vec3<f32>) -> vec3<f32> {
|
||||
// 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<f32>) -> vec3<f32> {
|
||||
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<f32>) -> @location(0) vec4<f32> {
|
||||
let d = textureLoad(data_tex, vec2<i32>(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>(i32(pos.x), i32(pos.y)), 0).b != 0. {
|
||||
return vec4<f32>(0., 0.11, 0.54, 1.0);
|
||||
} else {
|
||||
let d = array<vec3<f32>, 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<f32>(col, 1.0);
|
||||
let normal = normalize(cross(d[1] - d[0], d[2] - d[0]));
|
||||
|
||||
var color: vec3<f32>;
|
||||
if u.shadow_palette_id == 0u {
|
||||
color = compute_light(normal,vec3<f32>(.5, .5, .5));
|
||||
} else {
|
||||
color = compute_light(normal, vec3<f32>(0., .5, .5)) * vec3<f32>(1., 0.5, 0.5) + compute_light(normal, vec3<f32>(0.5, 0., .5)) * vec3<f32>(0.5, 1., 1.);
|
||||
|
||||
color = filmic(color);
|
||||
}
|
||||
|
||||
return vec4<f32>(color, 1.0);
|
||||
}
|
||||
} else {
|
||||
let d = textureLoad(data_tex, vec2<i32>(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<f32>(col, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<f32>,
|
||||
// 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<f32>, 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user