Ray-traced shadows and reflections in a raster app
A normal real-time three.js scene (WebGPU raster, 60 fps) where two effects are ray traced at
one ray per pixel: the shadow of a big soft light, and glossy reflections. One ray is
far too noisy to look at. ffxShadows() and ffxReflections() from
denoiser/three (AMD FidelityFX denoisers, ported to WGSL) clean it up every frame.
Compare each one with what three.js gives you without ray tracing.
Shadows: ...Reflections: ...
Shadows
Light size
Look where the table and chair legs touch the floor.
Reflections
Floor gloss
roughness of the floor and table top
Resolution
Camera
What it costs on your GPU
measuring...
Measured live from GPU timestamp queries for the modes selected above, at 960x540 (1080p has 4x the pixels).
Passes a mode doesn't need aren't run. GPUs overlap passes, so each row is an estimate. The frame time is measured directly.
When to use this
You ship a real-time raster app (a game, a product configurator, an archviz walkthrough) that has to hold 60 fps while the camera moves.
You want one effect raster can't fake well: soft area-light shadows that stay sharp where objects touch the ground and soften with distance, or glossy reflections of the actual scene instead of an env map.
So you trace 1 ray per pixel for just that effect, and let the denoiser turn the noise into a clean, stable result every frame.
When to use OIDN instead
You path trace the whole frame (all lighting, bounces, glass), for example a product render or a viewer that refines when the camera stops.
That's Denoiser, the package's main API (OIDN): one whole image, no history needed, tens of milliseconds per denoise. Run it on a still frame or every few frames, not every frame at 60 fps.
FFX does the opposite: one effect, every frame, about 0.5 ms (shadows) or 1.7 ms (reflections) at 1080p, using motion vectors and history.
What it costs
Ray tracing: 1 ray per pixel against the scene's BVH. Here that's three-mesh-bvh's WebGPU traversal in a full-screen TSL pass, no ray-tracing hardware needed. The table above shows what it costs for this scene.
Denoising: FFX shadows about 0.5 ms and FFX reflections about 1.7 ms at 1080p (Apple M-series). The cost depends on resolution, not on the scene.
No precomputation or baking. Lights, objects and the camera can all move.
What the denoiser needs from you
From your G-buffer: depth, normals (view or world, packed is fine), motion vectors (three's velocity in the MRT), and roughness for reflections.
An honest 1-ray signal: new random samples every frame, not pre-blurred. For shadows that's 0 or 1 per pixel. For reflections it's the radiance plus the hit distance (three's stochastic SSR gives you both).
Call resetHistory() on camera cuts and teleports, or the old frame bleeds into the new one.
// visibility: a TSL node or texture, 1 = lit, 0 = shadowed, 1 ray/pixel
const shadows = ffxShadows(visibility, {
camera, depth: scenePass.getTextureNode('depth'), velocity: scenePass.getTextureNode('velocity'),
normal: scenePass.getTextureNode('normal'), normalSpace: 'view', normalEncoding: 'packed',
});
const lit = sceneColor.add(albedo.mul(irradiance).mul(shadows.r));
// on a camera cut:
shadows.resetHistory();