perf: improve 3d ray marching performance

This commit is contained in:
2026-09-24 19:45:17 +02:00
parent 5206a22bdd
commit 2bdb2356c6
3 changed files with 47 additions and 18 deletions
+7
View File
@@ -204,6 +204,13 @@ sample per pixel. When AA is on, `fs_refine` reads that texture and runs the
reads the refined texture. PNG export (`fs_color`) still supersamples every reads the refined texture. PNG export (`fs_color`) still supersamples every
pixel. pixel.
The 3D view (`colorize.wgsl::ray_marching`) sphere-traces the DE height
field straight from the data texture. It's cheap: rays start on the z = 0
plane, and most hit within a few steps (about 4 on average). A min-height
mip pyramid (quadtree height-field tracing) was tried and measured about 3×
slower, because it needs about 12 costlier steps per ray. Don't reintroduce it.
Orbiting the camera only re-runs the colourise pass, never iteration.
While the user is actively panning/zooming, the app renders downscaled with While the user is actively panning/zooming, the app renders downscaled with
AA off (`INTERACT_DOWNSCALE`) and snaps back to full resolution once input AA off (`INTERACT_DOWNSCALE`) and snaps back to full resolution once input
settles (`INTERACT_SETTLE`). settles (`INTERACT_SETTLE`).
+4 -1
View File
@@ -2127,9 +2127,12 @@ impl FractalApp {
if self.rendering_mode == 2 { if self.rendering_mode == 2 {
if let Some(mt) = multi_touch { if let Some(mt) = multi_touch {
let t = mt.translation_delta; let t = mt.translation_delta;
// Orbiting only moves the camera, which the colourise pass
// handles alone, so (like mouse-drag orbiting) it doesn't count
// as interaction: that would drop to the low-res pass and
// re-iterate the fractal twice.
if t.x != 0.0 || t.y != 0.0 { if t.x != 0.0 || t.y != 0.0 {
self.camera.rotate(-t.x * ROT_SENS, -t.y * ROT_SENS); self.camera.rotate(-t.x * ROT_SENS, -t.y * ROT_SENS);
interacted = true;
} }
if mt.zoom_delta != 1.0 { if mt.zoom_delta != 1.0 {
let off = mt.center_pos - rect.center(); let off = mt.center_pos - rect.center();
+37 -18
View File
@@ -68,6 +68,9 @@ fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
} }
} }
// Colour of rays that miss the fractal's footprint.
const RAY_MISS: vec4<f32> = vec4<f32>(1.0, 0.0, 0.0, 1.0);
// Per-frame constants of the raymarch, computed once per pixel in // Per-frame constants of the raymarch, computed once per pixel in
// `ray_marching` rather than on each of the up-to-100 `sdf` steps. // `ray_marching` rather than on each of the up-to-100 `sdf` steps.
struct MarchConsts { struct MarchConsts {
@@ -126,27 +129,43 @@ fn ray_marching(pos: vec4<f32>) -> vec4<f32> {
let ray_origin = world_pos.xyz; let ray_origin = world_pos.xyz;
let ray_dir = u.camera_direction; let ray_dir = u.camera_direction;
let z_intersect = ray_origin.z / ray_dir.z; // Start where the ray crosses z = 0, the topmost possible surface (the
var p = ray_origin - ray_dir * z_intersect; // camera pitch is clamped short of ±90°, so ray_dir.z > 0).
var i = 0u; let start = ray_origin - ray_dir * (ray_origin.z / ray_dir.z);
var dist = 0.0;
let dist_threshold = 0.000001; // The terrain only exists over the footprint x in [0, aspect],
while i < 100u { // y in [0, 1]: clip the ray's xy to it up front, so rays that miss it cost
let from_origin = p - ray_origin; // nothing and the rest start marching at its edge. A huge finite 1/d on
if dot(from_origin, from_origin) > 9. { // an axis the ray doesn't move along (top-down, during the 2D <-> 3D
break; // transition) keeps the slab maths finite.
} let inv = select(1.0 / ray_dir.xy, vec2<f32>(1e30), abs(ray_dir.xy) < vec2<f32>(1e-20));
dist = sdf(p, k); let ta = -start.xy * inv;
if dist < dist_threshold { let tb = (vec2<f32>(aspect_ratio, 1.0) - start.xy) * inv;
break; let t_leave = min(max(ta.x, tb.x), max(ta.y, tb.y));
} var t = max(max(min(ta.x, tb.x), min(ta.y, tb.y)), 0.0);
p += dist * ray_dir; if t >= t_leave {
i += 1u; return RAY_MISS;
} }
// About 1/50 of a texel at typical sizes: tighter only adds steps
// without visibly moving the hit.
let dist_threshold = 0.00001;
var hit = false;
for (var i = 0u; i < 100u; i++) {
let dist = sdf(start + t * ray_dir, k);
if dist < dist_threshold { if dist < dist_threshold {
return shadow_fragment(p.xy * k.to_texel); hit = true;
break;
} }
return vec4<f32>(1., 0., 0., 1.); t += dist;
// Past the footprint's far edge: nothing left to hit.
if t >= t_leave {
break;
}
}
// Shade outside the loop, so its registers don't weigh on the march.
if !hit {
return RAY_MISS;
}
return shadow_fragment((start.xy + t * ray_dir.xy) * k.to_texel);
} }