perf: editing theme doesn't require a complete reredenring
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
// Colourise pass: map the iteration pass's per-pixel escape data (from
|
||||
// `mandelbrot.wgsl`'s `fs_data`) through the palette. This is the only
|
||||
// color-dependent step, so changing the palette / colour scale / offset (e.g.
|
||||
// colour cycling) re-runs just this cheap pass — the expensive perturbation
|
||||
// iteration in the data texture is reused untouched.
|
||||
//
|
||||
// The data texture holds, per texel: R = ci (palette parameter), G = DE
|
||||
// darkening factor, B = interior fraction (for boundary anti-aliasing). It is
|
||||
// the same resolution as this pass's target, so we read it with `textureLoad`
|
||||
// at the fragment's integer pixel coordinate (nearest — iteration data must not
|
||||
// be linearly filtered across escape boundaries).
|
||||
|
||||
// Must match `Uniforms` in mandelbrot.wgsl / the Rust `Uniforms` struct.
|
||||
struct Uniforms {
|
||||
span: vec2<f32>,
|
||||
max_iter: u32,
|
||||
ref_len: u32,
|
||||
color_offset: f32,
|
||||
color_scale: f32,
|
||||
bailout_sq: f32,
|
||||
is_julia: u32,
|
||||
palette_id: u32,
|
||||
aa_level: u32,
|
||||
kind: u32,
|
||||
power: u32,
|
||||
dc_offset: vec2<f32>,
|
||||
phoenix_p: vec2<f32>,
|
||||
de_coloring: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||
@group(0) @binding(1) var data_tex: texture_2d<f32>;
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
d = vec3<f32>(0.00, 0.33, 0.67); // rainbow
|
||||
} else if (id == 2u) {
|
||||
d = vec3<f32>(0.30, 0.20, 0.20); // warm ember
|
||||
} else if (id == 3u) {
|
||||
c = vec3<f32>(1.0, 1.0, 0.5);
|
||||
d = vec3<f32>(0.80, 0.90, 0.30); // lime / magenta
|
||||
}
|
||||
return a + b * cos(6.28318530718 * (c * t + d));
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
|
||||
var verts = array<vec2<f32>, 3>(
|
||||
vec2<f32>(-1.0, -1.0),
|
||||
vec2<f32>(3.0, -1.0),
|
||||
vec2<f32>(-1.0, 3.0),
|
||||
);
|
||||
return vec4<f32>(verts[idx], 0.0, 1.0);
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
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);
|
||||
}
|
||||
+75
-27
@@ -193,12 +193,21 @@ fn palette(id: u32, t: f32) -> vec3<f32> {
|
||||
return a + b * cos(6.28318530718 * (c * t + d));
|
||||
}
|
||||
|
||||
// Perturbation iterate + color a single sample. `offset` is the per-pixel
|
||||
// offset in complex units. For Mandelbrot it is the c-plane offset added every
|
||||
// step (delta starts at 0); for Julia it is the z-plane offset that seeds the
|
||||
// initial delta (c is fixed, so nothing is added per step). Interior pixels
|
||||
// return black.
|
||||
fn shade(offset: vec2<f32>, px: f32) -> vec3<f32> {
|
||||
// Escape data for one sample: `ci` is the (color-independent) palette parameter,
|
||||
// `de` the distance-estimate darkening factor in [0,1], `escaped` false for the
|
||||
// interior of the set. Splitting iteration from coloring lets a colour change be
|
||||
// remapped cheaply (see the colourise pass) without re-iterating.
|
||||
struct Sample {
|
||||
ci: f32,
|
||||
de: f32,
|
||||
escaped: bool,
|
||||
};
|
||||
|
||||
// Perturbation iterate a single sample. `offset` is the per-pixel offset in
|
||||
// complex units. For Mandelbrot it is the c-plane offset added every step (delta
|
||||
// starts at 0); for Julia it is the z-plane offset that seeds the initial delta
|
||||
// (c is fixed, so nothing is added per step).
|
||||
fn iterate_sample(offset: vec2<f32>, px: f32) -> Sample {
|
||||
let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia)
|
||||
|
||||
var step_add = offset;
|
||||
@@ -282,7 +291,7 @@ fn shade(offset: vec2<f32>, px: f32) -> vec3<f32> {
|
||||
}
|
||||
|
||||
if (!escaped) {
|
||||
return vec3<f32>(0.0, 0.0, 0.0); // interior of the set
|
||||
return Sample(0.0, 1.0, false); // interior of the set
|
||||
}
|
||||
|
||||
let z2 = dot(z, z);
|
||||
@@ -295,9 +304,8 @@ fn shade(offset: vec2<f32>, px: f32) -> vec3<f32> {
|
||||
// sqrt compresses the huge iteration counts of deep zooms so the palette
|
||||
// varies smoothly instead of aliasing into speckle.
|
||||
let ci = sqrt(max(smooth_i, 0.0));
|
||||
let t = fract(ci * u.color_scale + u.color_offset);
|
||||
var col = palette(u.palette_id, t);
|
||||
|
||||
var de = 1.0;
|
||||
if (u.de_coloring != 0u) {
|
||||
// Exterior distance estimate (complex-plane units): |z|·ln|z| / |dz|.
|
||||
// Divided by the pixel footprint it becomes a distance in pixels; we
|
||||
@@ -306,37 +314,77 @@ fn shade(offset: vec2<f32>, px: f32) -> vec3<f32> {
|
||||
// boundary simply reads as dark, which is the correct limit.
|
||||
let zmag = sqrt(max(z2, 1.0));
|
||||
let dzmag = sqrt(max(dot(dz, dz), 1e-20));
|
||||
let de = zmag * log(zmag) / dzmag;
|
||||
let de_px = de / max(px, 1e-30);
|
||||
col = col * clamp(de_px, 0.0, 1.0);
|
||||
let d = zmag * log(zmag) / dzmag;
|
||||
de = clamp(d / max(px, 1e-30), 0.0, 1.0);
|
||||
}
|
||||
return col;
|
||||
return Sample(ci, de, true);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let base = in.centered * u.span + u.dc_offset;
|
||||
// Map a sample's escape data through the palette (+ DE darkening). This is the
|
||||
// only color-dependent step, so it can be redone without re-iterating. Interior
|
||||
// samples are black.
|
||||
fn color_sample(s: Sample) -> vec3<f32> {
|
||||
if (!s.escaped) {
|
||||
return vec3<f32>(0.0, 0.0, 0.0);
|
||||
}
|
||||
let t = fract(s.ci * u.color_scale + u.color_offset);
|
||||
return palette(u.palette_id, t) * s.de;
|
||||
}
|
||||
|
||||
// Screen-space complex-units-per-pixel. Derivatives must be evaluated in
|
||||
// uniform control flow, so take them here; used to place sub-pixel AA
|
||||
// samples and to convert the distance estimate into pixels.
|
||||
// Iteration pass: write per-pixel escape data (color-independent) so a colour
|
||||
// change is remapped by the cheap colourise pass without re-iterating.
|
||||
// R = ci (palette parameter), G = DE factor, B = interior fraction (for AA).
|
||||
// AA is grid-supersampled here; the interior fraction lets the colourise pass
|
||||
// anti-alias the set boundary (blend toward black) after the fact.
|
||||
@fragment
|
||||
fn fs_data(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let base = in.centered * u.span + u.dc_offset;
|
||||
let dx = dpdx(base);
|
||||
let dy = dpdy(base);
|
||||
let px = length(abs(dx) + abs(dy)); // ~ complex units per pixel (footprint)
|
||||
let px = length(abs(dx) + abs(dy));
|
||||
|
||||
let aa = max(u.aa_level, 1u);
|
||||
if (aa <= 1u) {
|
||||
return vec4<f32>(shade(base, px), 1.0);
|
||||
}
|
||||
|
||||
var acc = vec3<f32>(0.0, 0.0, 0.0);
|
||||
let inv = 1.0 / f32(aa);
|
||||
var ci_sum = 0.0;
|
||||
var de_sum = 0.0;
|
||||
var escaped_n = 0u;
|
||||
for (var sy: u32 = 0u; sy < aa; sy = sy + 1u) {
|
||||
for (var sx: u32 = 0u; sx < aa; sx = sx + 1u) {
|
||||
// Sample centers evenly spread across the pixel, jitter in (-0.5, 0.5).
|
||||
let jx = (f32(sx) + 0.5) * inv - 0.5;
|
||||
let jy = (f32(sy) + 0.5) * inv - 0.5;
|
||||
acc = acc + shade(base + jx * dx + jy * dy, px);
|
||||
let s = iterate_sample(base + jx * dx + jy * dy, px);
|
||||
if (s.escaped) {
|
||||
ci_sum = ci_sum + s.ci;
|
||||
de_sum = de_sum + s.de;
|
||||
escaped_n = escaped_n + 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
let total = f32(aa * aa);
|
||||
let ci_avg = select(0.0, ci_sum / f32(escaped_n), escaped_n > 0u);
|
||||
let de_avg = select(1.0, de_sum / f32(escaped_n), escaped_n > 0u);
|
||||
let interior_frac = 1.0 - f32(escaped_n) / total;
|
||||
return vec4<f32>(ci_avg, de_avg, interior_frac, 1.0);
|
||||
}
|
||||
|
||||
// Combined iterate + colour in a single pass, for PNG export (which never needs
|
||||
// incremental recolouring). The interactive path uses fs_data + the colourise
|
||||
// pass so colour changes skip iteration.
|
||||
@fragment
|
||||
fn fs_color(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let base = in.centered * u.span + u.dc_offset;
|
||||
let dx = dpdx(base);
|
||||
let dy = dpdy(base);
|
||||
let px = length(abs(dx) + abs(dy));
|
||||
|
||||
let aa = max(u.aa_level, 1u);
|
||||
let inv = 1.0 / f32(aa);
|
||||
var acc = vec3<f32>(0.0, 0.0, 0.0);
|
||||
for (var sy: u32 = 0u; sy < aa; sy = sy + 1u) {
|
||||
for (var sx: u32 = 0u; sx < aa; sx = sx + 1u) {
|
||||
let jx = (f32(sx) + 0.5) * inv - 0.5;
|
||||
let jy = (f32(sy) + 0.5) * inv - 0.5;
|
||||
acc = acc + color_sample(iterate_sample(base + jx * dx + jy * dy, px));
|
||||
}
|
||||
}
|
||||
return vec4<f32>(acc / f32(aa * aa), 1.0);
|
||||
|
||||
Reference in New Issue
Block a user