SSAO — screen-space ambient occlusion
Purpose
SSAO is fake soft shadowing that uses only the depth of the scene to figure out where surfaces are buried near each other — which is why a PBR scene without it looks floaty and a PBR scene with it feels grounded.
Key insight
Objects sitting on a floor, a book's spine against a shelf, the crease between a nose and a cheek — these places are darker in real life because surrounding geometry blocks some of the ambient light from reaching them. Computing real shadows for ambient light is expensive. SSAO cheats: for every pixel on screen, it samples a small hemisphere of nearby points and asks "is this sample buried inside scene geometry?" by checking the depth buffer. The fraction of samples that are buried becomes an occlusion value — darker where many samples are buried (deep creases, tight corners), lighter where few are (flat open surfaces).
Two things make this lesson conceptually distinct from every prior one. First, the shader reads from a second buffer — not color, but depth — which means the modern renderer outputs multiple images per pixel per frame. That collection is called the G-buffer. Second, because SSAO samples neighbors, it inherits the Lesson-1 multi-pass contract: this cannot be one-pass. Depth is rendered first, then SSAO samples from it, then the blur smooths the noisy samples, then the composite multiplies occlusion into the color.
Break it
Pull groundedness to 0. The sphere visibly floats — no soft contact shadow beneath it, no darkening where it kisses the plane, no crease where the cube meets the ground. The brain reads the sphere and cube as composited-on-top rather than sitting-on. Now slide up to a natural mid-value: they lock to the ground. Teaches: ambient occlusion is the single biggest contributor to "objects belong to each other." Without it, even perfect PBR looks like a collage. With it, even simple lighting feels physical. When a scene feels floaty, ask for "more SSAO" before adjusting any light.
Direct Claude
// SSAO needs the G-buffer: depth + normals + color.
// Three.js's SSAOPass handles all three internally — it re-renders the scene
// into depth + normal render targets, then samples from them.
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// SSAOPass needs scene+camera because it renders the G-buffer itself.
const ssao = new SSAOPass(scene, camera, width, height);
// These parameters ARE the SSAO character:
// kernelRadius — how far to look for occluders
// (the `groundedness` slider above drives this)
// minDistance — reject self-occlusion within this depth range
// maxDistance — reject occluders farther than this (prevents halos)
ssao.kernelRadius = 8.0;
ssao.minDistance = 0.002;
ssao.maxDistance = 0.1;
composer.addPass(ssao);
// Ordering rule: SSAO runs BEFORE bloom + tone-map + LUT.
// It's modifying the scene's lighting energy — the other passes work on the
// result of that. Try it in the other order and the AO darkening gets
// graded twice, or bloom sees wrong brightness, or tone-map flattens the
// contact shadow you wanted.
composer.addPass(new UnrealBloomPass(...)); // L32
composer.addPass(new OutputPass()); // L33 (ACES tone-map)
composer.addPass(lutPass); // L34
// Slider: `groundedness` scales kernelRadius.
// At radius 0 the sample hemisphere collapses to a point and no occlusion
// is detected. Slide up and the crevices show their darkening.
function setGroundedness(v) { ssao.kernelRadius = v * 16.0; }