Computer Graphics
Screen-Space Ambient Occlusion (SSAO): Faking Contact Shadows from the Depth Buffer
In 2007, Crysis shipped with a rendering trick that darkened the crease where a rock meets the ground, the inside of a jungle-plant leaf, the seam between a soldier's arm and torso — all without tracing a single ray or precomputing a single light map. That trick was Screen-Space Ambient Occlusion (SSAO), and it works by looking only at the depth buffer the GPU already produced.
SSAO is a real-time approximation of ambient occlusion: the soft, contact darkening that happens when nearby geometry blocks the diffuse skylight reaching a surface point. Instead of the exact hemispherical integral over surrounding geometry (which needs the whole scene), SSAO estimates occlusion per pixel using only the 2.5D depth and normal information visible on screen. It runs as a full-screen post-process in O(pixels × samples) and turns "the GPU already knows how far every pixel is from the camera" into free-looking contact shadows.
- TypeReal-time screen-space rendering technique (post-process)
- Time complexityO(W·H·N) — N depth samples per pixel, typically 8–64
- SpaceO(W·H) — one AO buffer plus depth/normal G-buffer inputs
- InventedVladimir Kajalin at Crytek; presented by Martin Mittring, SIGGRAPH 2007
- First shipped inCrysis (2007), CryEngine 2
- Key ideaEstimate occlusion per pixel from the depth buffer's 2.5D neighborhood, not full 3D scene
Interactive visualization
Press play, or step through manually. The visualization is yours to drive — try it before reading on.
Watch the 60-second explainer
A condensed visual walkthrough — narrated, captioned, under a minute.
The problem: ambient light has no shadows unless you fake them
In a physically correct render, every surface point receives light not just from lamps but from the whole sky and every bounce off nearby surfaces. Where geometry crowds together — a corner, a crack, the underside of a bolt — that ambient light is partially blocked, so those spots look darker. This soft self-shadowing, called ambient occlusion (AO), is what makes CG images read as solid instead of flat and plasticky.
The exact AO at a point is a hemispherical integral over all directions, testing which are blocked by other geometry:
AO(p) = 1 - (1/π) ∫_Ω V(p, ω) · cos θ dωwhere V is the visibility (0 if blocked, 1 if open) in direction ω. Computing V exactly means intersecting rays against the entire scene — far too slow for a 60 fps game in 2007, and it needs geometry that may be off-screen or unloaded. SSAO's insight: the GPU already rasterized a depth buffer holding the camera-space distance of every visible pixel. That depth buffer is a cheap, approximate stand-in for the nearby geometry, so you can estimate the occlusion integral from it alone — no scene traversal, no precomputation, works on animated and deforming meshes for free.
How SSAO works, step by step
SSAO runs as a full-screen pass after the depth (and usually normal) G-buffer is filled. For every pixel P it does roughly this:
- Reconstruct position. Read P's depth, unproject to a view-space position
pand read its normaln. - Generate a sample kernel. Take N random offset vectors distributed in a hemisphere oriented around
n(classic Crytek SSAO used a full sphere), scaled by a small world radius r. - Project & test each sample. For each offset, compute the 3D sample point
s = p + offset, project it back to screen space, and read the stored depth at that screen location. - Occlusion vote. If the stored geometry there is closer to the camera than
s(i.e. something is in front of the sample), that sample is occluded. - Range check & accumulate. Weight the vote so occluders far in depth don't count (prevents distant background darkening foreground). AO = fraction of samples occluded.
occlusion = 0
for i in 1..N:
s = p + rotate(kernel[i], randomVec) * radius
d = depthBuffer[ project(s).xy ] // stored scene depth
rangeCheck = smoothstep(0,1, radius / abs(p.z - d))
if d >= s.z + bias: // occluder in front of sample
occlusion += rangeCheck
AO = 1 - occlusion / NA per-pixel random rotation decorrelates the kernel so artifacts become high-frequency noise, then a small bilateral (depth-aware) blur smooths that noise without bleeding across depth edges.
Complexity and a worked step-trace
Let the framebuffer be W×H pixels and N the samples per pixel. Each sample is a projection plus one texture fetch, so:
- Time: O(W·H·N). At 1920×1080 with N=16 that is ~33M sample fetches per frame — cheap on a GPU that does this massively in parallel. The blur adds O(W·H·k) for a k-tap kernel.
- Space: O(W·H): one single-channel AO target, on top of the depth/normal buffers you already have. No scene-size term at all — SSAO's cost is independent of triangle count, which is its headline win.
Worked trace for one pixel P at view-space depth p.z = 10.0, radius r = 0.5, bias = 0.025, N = 4:
sample 1 → s.z = 10.30, stored depth = 10.28 → 10.28 < 10.30+bias? occluder in front → OCCLUDED
sample 2 → s.z = 9.72, stored depth = 9.95 → geometry is behind sample → open
sample 3 → s.z = 10.10, stored depth = 10.05 → 10.05 < 10.10 → OCCLUDED
sample 4 → s.z = 9.85, stored depth = 9.88 → geometry behind sample → open
occluded = 2 of 4 → AO = 1 - 2/4 = 0.5 (surface half-darkened)Because N is tiny, low counts look noisy — hence the random rotation + bilateral blur that trades N up to an effective higher sample count across neighboring pixels.
Where SSAO is used in real systems
SSAO and its descendants are standard in essentially every real-time renderer:
- Game engines. Unreal Engine ships SSAO/GTAO, Unity has SSAO in its post-process stack, and CryEngine originated it. Godot offers SSAO out of the box.
- Deferred renderers. SSAO is a natural fit for deferred shading pipelines, which already produce depth+normal G-buffers; the AO pass just consumes them before the lighting pass and modulates the ambient/indirect term.
- The web & browsers. Three.js exposes
SAOPassandSSAOPassin its post-processing (EffectComposer); Babylon.js has an SSAO2 rendering pipeline. This is what gives WebGL product configurators and architectural viewers their grounded look. - DCC & offline previews. Blender's EEVEE, viewport renderers, and CAD tools use screen-space AO for fast, interactive shading feedback.
Vendors shipped drop-in libraries too: NVIDIA's HBAO+ and AMD's CACAO/FidelityFX are production SSAO-family effects tuned for stability and speed, forced on in many driver control panels regardless of what the game itself implements.
SSAO versus the alternatives, and the tradeoff
The AO-technique landscape is a ladder of accuracy-for-cost:
- Classic SSAO is the cheapest and the crudest — point-sampling a sphere gives a rough occlusion count that must be heavily blurred, and it produces the infamous dark halos around silhouettes and gray fringes on flat walls.
- HBAO (Bavoil et al., NVIDIA 2008) is smarter: instead of scattered points it ray-marches the depth buffer along a few directions to find the largest horizon angle, then integrates the occluded arc. This respects geometry and yields cleaner contact shadows at 2–3× the cost.
- GTAO (Jimenez et al., Activision 2016) refines the horizon integral and calibrates it against offline ray-traced ground truth, adding a multi-bounce approximation. It is the most physically plausible and the most temporally stable in motion.
- Ray-traced AO (RTAO) casts real rays into a BVH of the full scene, eliminating screen-space artifacts entirely — but needs RT hardware and a denoiser.
The core tradeoff: everything screen-space is fast and scene-size-independent but fundamentally can only see what's on screen. RTAO sees the whole scene but pays for it in rays. SSAO trades correctness for a nearly free, universally applicable approximation.
Pitfalls, failure modes, and why it still matters
SSAO is a hack, and its artifacts all stem from working in 2.5D screen space rather than true 3D:
- Missing / off-screen occluders. Geometry outside the frustum, or hidden behind a nearer surface, contributes zero occlusion — so AO pops as objects scroll on and off screen.
- Halos and dark edges. A foreground object incorrectly darkens the distant background around its silhouette because the depth gap fools the range check; too-small a bias creates self-occlusion (a surface shadowing itself, causing gray banding on flat walls).
- Depth precision & thin objects. Thin occluders (wires, foliage) can be under- or over-counted; near/far depth precision issues cause flicker.
- Temporal instability. Low sample counts plus per-pixel random rotation make AO shimmer under camera motion — the reason TAA or temporal accumulation is now paired with it.
- Resolution/radius dependence. The world radius maps to a variable pixel radius with distance, so tuning that looks right up close over-blurs far away.
Despite all that, SSAO's significance is enormous: it decoupled ambient-occlusion cost from scene complexity, made contact shadows viable in real time on 2007 hardware, and seeded an entire family (HBAO, HBAO+, GTAO, screen-space GI) that still ships in shipping AAA titles and browser 3D today.
| Technique | Year / Author | How it samples | Cost & quality tradeoff |
|---|---|---|---|
| Classic SSAO | 2007, Kajalin/Mittring (Crytek) | Random point samples in a sphere around the pixel; count how many are behind the depth buffer | Cheapest; noisy, prone to halos and self-occlusion; needs heavy blur |
| HBAO | 2008, Bavoil et al. (NVIDIA) | Ray-marches the depth buffer to find the max horizon angle per direction | ~2–3× SSAO cost; smoother, geometry-aware contact shadows |
| HBAO+ | ~2015, NVIDIA | Optimized HBAO with better sampling & interleaving | Higher detail than HBAO, runs several × faster than plain HBAO |
| GTAO | 2016, Jimenez et al. (Activision) | Horizon integral calibrated to match offline ray-traced ground truth | Most physically accurate & motion-stable; highest of the four |
| Ray-traced AO (RTAO) | 2018+, hardware RT | Casts actual rays into full 3D scene (BVH) | No screen-space artifacts; needs RT cores + denoiser; most expensive |
Frequently asked questions
Why does SSAO only need the depth buffer and not the whole 3D scene?
The depth buffer already encodes the camera-space distance of every visible pixel, which is a cheap 2.5D approximation of the nearby geometry. SSAO reconstructs each pixel's view-space position from depth and tests random neighbor samples against stored depths to guess occlusion. This makes its cost independent of triangle count, but it also means it can only account for geometry that is actually visible on screen.
What causes the halo artifacts around objects in SSAO?
Halos happen when a foreground object's depth is compared against a much farther background pixel. The large depth gap makes samples read as occluders, darkening a ring of background around the silhouette. Classic SSAO mitigates this with a range check (smoothstep on depth difference) that fades out occlusion from far-away depths, and HBAO/GTAO reduce it further with horizon-angle sampling.
What is the difference between SSAO, HBAO, and GTAO?
All three are screen-space AO. SSAO point-samples a sphere/hemisphere and counts occluded samples — cheap but noisy. HBAO (NVIDIA, 2008) ray-marches the depth buffer to find the horizon angle per direction, giving smoother, geometry-aware results at higher cost. GTAO (Activision, 2016) integrates the horizon and is calibrated against offline ray-traced ground truth, making it the most physically accurate and motion-stable.
Why do SSAO implementations apply a blur, and why must it be depth-aware?
With only 8–64 samples per pixel, the raw AO is noisy, so a blur averages neighboring pixels to raise the effective sample count. It must be a bilateral (depth-aware) blur so it doesn't smear AO across depth discontinuities — a plain Gaussian would bleed the AO of a foreground object onto the background, reintroducing halos and softening contact shadow edges.
How do you pick the SSAO sample radius and bias?
The radius is a world-space distance defining how far to look for occluders — too large captures broad shading (looks like GI), too small captures only tight creases. The bias is a small depth offset added before the occlusion test to prevent a flat surface from shadowing itself due to depth precision. Too little bias causes gray self-occlusion banding; too much erases valid contact shadows. Both are usually scaled with distance.
Is SSAO obsolete now that GPUs have ray tracing?
No. Ray-traced AO (RTAO) removes screen-space artifacts but requires RT hardware, a BVH of the scene, and a denoiser, and it is far more expensive per frame. SSAO-family techniques (especially GTAO and vendor libraries like HBAO+ and FidelityFX CACAO) remain the default on most hardware because they are nearly free, scale independently of scene complexity, and run everywhere from mobile to WebGL.