diff --git a/src/app.rs b/src/app.rs index 28b108b..39d889c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -52,6 +52,8 @@ pub struct FractalApp { color_scale: f32, color_offset: f32, palette: u32, + /// Supersample each pixel 2×2 for smoother edges (costs ~4× fragment work). + antialias: bool, /// Reference orbit (`Z_n` as f32 pairs) for the current view. reference: Arc>, @@ -107,6 +109,7 @@ impl FractalApp { color_scale: 0.15, color_offset: 0.0, palette: 0, + antialias: false, reference: Arc::new(Vec::new()), generation: 0, ref_center_re, @@ -377,7 +380,7 @@ impl FractalApp { bailout_sq: BAILOUT_SQ, is_julia: matches!(self.mode, FractalMode::Julia) as u32, palette_id: self.palette, - _pad0: 0, + aa_level: if self.antialias { 2 } else { 1 }, dc_offset: self.dc_offset(), } } @@ -484,6 +487,8 @@ impl FractalApp { ui.selectable_value(&mut self.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.separator(); let (cre, cim) = self.view.center_f64(); diff --git a/src/fractal/renderer.rs b/src/fractal/renderer.rs index bf2f61d..77120a4 100644 --- a/src/fractal/renderer.rs +++ b/src/fractal/renderer.rs @@ -30,7 +30,8 @@ pub struct Uniforms { /// 0 = Mandelbrot, 1 = Julia. pub is_julia: u32, pub palette_id: u32, - pub _pad0: u32, + /// Supersampling factor per axis: 1 = off, 2 = 2×2 (4 samples). + pub aa_level: u32, /// 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. diff --git a/src/shaders/mandelbrot.wgsl b/src/shaders/mandelbrot.wgsl index 08c0c9e..1977a1f 100644 --- a/src/shaders/mandelbrot.wgsl +++ b/src/shaders/mandelbrot.wgsl @@ -21,7 +21,7 @@ struct Uniforms { bailout_sq: f32, is_julia: u32, palette_id: u32, - _pad0: u32, + aa_level: u32, dc_offset: vec2, }; @@ -74,12 +74,12 @@ fn palette(id: u32, t: f32) -> vec3 { return a + b * cos(6.28318530718 * (c * t + d)); } -@fragment -fn fs_main(in: VsOut) -> @location(0) vec4 { - // Per-pixel offset. For Mandelbrot this 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). - let offset = in.centered * u.span + u.dc_offset; +// 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) -> vec3 { let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia) var step_add = offset; @@ -130,7 +130,7 @@ fn fs_main(in: VsOut) -> @location(0) vec4 { } if (!escaped) { - return vec4(0.0, 0.0, 0.0, 1.0); // interior of the set + return vec3(0.0, 0.0, 0.0); // interior of the set } // Continuous (smooth) iteration count. @@ -142,5 +142,32 @@ fn fs_main(in: VsOut) -> @location(0) vec4 { // 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); - return vec4(palette(u.palette_id, t), 1.0); + return palette(u.palette_id, t); +} + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4 { + let base = in.centered * u.span + u.dc_offset; + + let aa = max(u.aa_level, 1u); + if (aa <= 1u) { + return vec4(shade(base), 1.0); + } + + // Screen-space complex-units-per-pixel, used to place sub-pixel samples. + // Derivatives must be evaluated in uniform control flow, so take them here. + let dx = dpdx(base); + let dy = dpdy(base); + + var acc = vec3(0.0, 0.0, 0.0); + let inv = 1.0 / f32(aa); + 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); + } + } + return vec4(acc / f32(aa * aa), 1.0); }