feat: add SSAA

This commit is contained in:
2026-09-15 09:07:21 +02:00
parent 10fc122e9c
commit 343ddfc4b7
3 changed files with 44 additions and 11 deletions
+6 -1
View File
@@ -52,6 +52,8 @@ pub struct FractalApp {
color_scale: f32, color_scale: f32,
color_offset: f32, color_offset: f32,
palette: u32, 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 orbit (`Z_n` as f32 pairs) for the current view.
reference: Arc<Vec<[f32; 2]>>, reference: Arc<Vec<[f32; 2]>>,
@@ -107,6 +109,7 @@ impl FractalApp {
color_scale: 0.15, color_scale: 0.15,
color_offset: 0.0, color_offset: 0.0,
palette: 0, palette: 0,
antialias: false,
reference: Arc::new(Vec::new()), reference: Arc::new(Vec::new()),
generation: 0, generation: 0,
ref_center_re, ref_center_re,
@@ -377,7 +380,7 @@ impl FractalApp {
bailout_sq: BAILOUT_SQ, bailout_sq: BAILOUT_SQ,
is_julia: matches!(self.mode, FractalMode::Julia) as u32, is_julia: matches!(self.mode, FractalMode::Julia) as u32,
palette_id: self.palette, palette_id: self.palette,
_pad0: 0, aa_level: if self.antialias { 2 } else { 1 },
dc_offset: self.dc_offset(), dc_offset: self.dc_offset(),
} }
} }
@@ -484,6 +487,8 @@ impl FractalApp {
ui.selectable_value(&mut self.palette, i as u32, *name); 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(); ui.separator();
let (cre, cim) = self.view.center_f64(); let (cre, cim) = self.view.center_f64();
+2 -1
View File
@@ -30,7 +30,8 @@ pub struct Uniforms {
/// 0 = Mandelbrot, 1 = Julia. /// 0 = Mandelbrot, 1 = Julia.
pub is_julia: u32, pub is_julia: u32,
pub palette_id: 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 /// Complex offset of the view center from the reference center, so a stale
/// or reused reference (computed at a slightly different center) still maps /// or reused reference (computed at a slightly different center) still maps
/// correctly. Added to every pixel's per-pixel offset. /// correctly. Added to every pixel's per-pixel offset.
+36 -9
View File
@@ -21,7 +21,7 @@ struct Uniforms {
bailout_sq: f32, bailout_sq: f32,
is_julia: u32, is_julia: u32,
palette_id: u32, palette_id: u32,
_pad0: u32, aa_level: u32,
dc_offset: vec2<f32>, dc_offset: vec2<f32>,
}; };
@@ -74,12 +74,12 @@ fn palette(id: u32, t: f32) -> vec3<f32> {
return a + b * cos(6.28318530718 * (c * t + d)); return a + b * cos(6.28318530718 * (c * t + d));
} }
@fragment // Perturbation iterate + color a single sample. `offset` is the per-pixel
fn fs_main(in: VsOut) -> @location(0) vec4<f32> { // offset in complex units. For Mandelbrot it is the c-plane offset added every
// 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
// step (delta starts at 0). For Julia it is the z-plane offset that seeds // initial delta (c is fixed, so nothing is added per step). Interior pixels
// the initial delta (c is fixed, so nothing is added per step). // return black.
let offset = in.centered * u.span + u.dc_offset; fn shade(offset: vec2<f32>) -> vec3<f32> {
let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia) let z0 = ref_orbit[0]; // reference start (0 for Mandelbrot, center for Julia)
var step_add = offset; var step_add = offset;
@@ -130,7 +130,7 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
} }
if (!escaped) { if (!escaped) {
return vec4<f32>(0.0, 0.0, 0.0, 1.0); // interior of the set return vec3<f32>(0.0, 0.0, 0.0); // interior of the set
} }
// Continuous (smooth) iteration count. // Continuous (smooth) iteration count.
@@ -142,5 +142,32 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
// varies smoothly instead of aliasing into speckle. // varies smoothly instead of aliasing into speckle.
let ci = sqrt(max(smooth_i, 0.0)); let ci = sqrt(max(smooth_i, 0.0));
let t = fract(ci * u.color_scale + u.color_offset); let t = fract(ci * u.color_scale + u.color_offset);
return vec4<f32>(palette(u.palette_id, t), 1.0); return palette(u.palette_id, t);
}
@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
let base = in.centered * u.span + u.dc_offset;
let aa = max(u.aa_level, 1u);
if (aa <= 1u) {
return vec4<f32>(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<f32>(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<f32>(acc / f32(aa * aa), 1.0);
} }