GTAO in WebGPU without a second scene render
· 1 min read · tsl, webgpu, ambient-occlusion, post-processing, performance
Ambient occlusion in Threejs-Punk costs one extra attachment, not a second scene render. Here is how, and the mistakes along the way.
Normals from the beauty pass #
GTAO reads depth and normals. Depth comes from the beauty pass. Normals are one more MRT attachment of the same pass, gated per material by depth write: rain, smoke and flames write no depth, so blending keeps the surface's normal under them.
AO then multiplies only the lit part: (beauty - emissive) * ao + emissive. Neon keeps its glow. The pre-pass it replaced was a full scene render of about 90 draws and about 1 ms of CPU.
Enough samples #
three's GTAO marches 3 directions by ceil(samples / 3) steps. At 6 samples (2 steps) it missed nearly every occluder: read back from the GPU, the frame was 1.5% darker with AO than without, as if it were off. At 16 samples (6 steps) with a 2 m radius, it was about 10%, with contact shadows under the car, along kerbs and at building bases.
Denoise, do not use TRAA #
The grain is filtered by a denoise node rendered once per frame at the AO resolution. three's example resolves the grain with TRAA, but that needs a normal and velocity pre-pass, a second scene render, and rain and smoke have no velocity, so they would ghost at speed.
Cost at 2880x1620: +1.6 ms at half resolution (Ultra), +0.85 ms at 0.35 (Medium).
Off means off #
With AO off, such as phones, Low quality, or once the adaptive controller sheds it, the normal attachment leaves the render target entirely. It saves about 0.1 ms on an Apple GPU, and more where the GPU stores every attachment to memory.
Related frame-time work is in the mip bloom post. The project is on its project page.
Related projects
- Threejs-Punk — Fully featured Racing Game