perf: improve 3d ray marching performance
This commit is contained in:
@@ -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
@@ -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();
|
||||||
|
|||||||
+36
-17
@@ -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;
|
||||||
|
let tb = (vec2<f32>(aspect_ratio, 1.0) - start.xy) * inv;
|
||||||
|
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);
|
||||||
|
if t >= t_leave {
|
||||||
|
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 {
|
||||||
|
hit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
t += dist;
|
||||||
|
// Past the footprint's far edge: nothing left to hit.
|
||||||
|
if t >= t_leave {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
p += dist * ray_dir;
|
|
||||||
i += 1u;
|
|
||||||
}
|
}
|
||||||
|
// Shade outside the loop, so its registers don't weigh on the march.
|
||||||
if dist < dist_threshold {
|
if !hit {
|
||||||
return shadow_fragment(p.xy * k.to_texel);
|
return RAY_MISS;
|
||||||
}
|
}
|
||||||
return vec4<f32>(1., 0., 0., 1.);
|
return shadow_fragment((start.xy + t * ray_dir.xy) * k.to_texel);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user