// 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, max_iter: u32, ref_len: u32, color_offset: f32, color_scale: f32, 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, }; struct Light { azimuth: f32, altitude: f32, color: u32, _pad: u32 }; @group(0) @binding(0) var u: Uniforms; @group(0) @binding(1) var data_tex: texture_2d; @group(0) @binding(2) var lights: array; // Smooth cyclic palettes (Inigo Quilez cosine palettes). Must match the palette // in mandelbrot.wgsl. fn palette(id: u32, t: f32) -> vec3 { 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 { d = vec3(0.00, 0.33, 0.67); // rainbow } else if id == 2u { d = vec3(0.30, 0.20, 0.20); // warm ember } else if id == 3u { c = vec3(1.0, 1.0, 0.5); d = vec3(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 { var verts = array, 3>( vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0), ); 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(max(0., dot(normal, normalize(light)))); } fn uncharted2tonemap(x: vec3) -> vec3 { 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, white_point: f32) -> 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(white_point)); return curr * white_scale; } fn s(color: vec3, k: f32, c: f32) -> vec3 { return 1. / (1. + exp(-k * (color - c))); } fn contrast(color: vec3, k: f32, c: f32) -> vec3 { let color_c = s(color, k, c); return (color_c - s(vec3(0), k, c)) / (s(vec3(1), k, c) - s(vec3(0), k, c)); } @fragment fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { if u.shadow != 0u { if textureLoad(data_tex, vec2(i32(pos.x), i32(pos.y)), 0).b != 0. { return vec4(0.1, 0.1, 0.1, 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 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)) + vec3(0.58, 0.85, 1.) * 0.2; color = filmic(color, 2.5); color = contrast(color, 4., 0.67); } else if u.shadow_palette_id == 1u { 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, 4.2); } else { color = vec3(0); var light_count = 0; for (var i = 0u ; i < 16; i++) { let light_color = unpack4x8unorm(lights[i].color); if any(light_color != vec4(0)) { light_count += 1; } color += compute_light(normal, vec3( cos(lights[i].azimuth) * cos(lights[i].altitude), sin(lights[i].azimuth) * cos(lights[i].altitude), sin(lights[i].altitude))) * light_color.xyz * light_color.a; } color = filmic(color, 1. + f32(light_count)); } 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); } }