# GTAO in WebGPU without a second scene render

> How Threejs-Punk gets ambient occlusion from the beauty pass's own normals, why it needs 16 samples, and why it denoises instead of using TRAA.

Canonical page: https://andersonmancini.dev/blog/threejs-punk-gtao-without-second-render

Published: 2026-09-03 · 1 min read · Tags: tsl, webgpu, ambient-occlusion, post-processing, performance

Ambient occlusion in [Threejs-Punk](https://andersonmancini.dev/blog/threejs-punk-racing-game.md) 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](https://andersonmancini.dev/blog/threejs-punk-mip-bloom.md). The project is on its [project page](https://andersonmancini.dev/projects/threejs-punk.md).

## Related projects

- [Threejs-Punk](https://andersonmancini.dev/projects/threejs-punk.md) — Fully featured Racing Game
