Ambient Occlusion: From SSAO to GTAO

🎨 Computer Graphics ⏱ ~9 min read Intermediate level
SSAO HBAO GTAO Screen Space Global Illumination

Ambient occlusion approximates one thing global illumination normally needs a full light transport simulation for: the soft darkening in corners, creases, and contact points where nearby geometry blocks incoming ambient light. Since Crysis popularized screen-space AO in 2007, the technique has moved through several generations — SSAO (Screen-Space Ambient Occlusion), HBAO, HBAO+, and today's GTAO — each one closing the gap between a cheap screen-space hack and the physically correct answer a path tracer would give.

TL;DR: Ambient occlusion darkens corners and contact points to fake the shadowing that full global illumination would compute, without tracing real light. Real-time techniques evolved from SSAO's noisy random hemisphere sampling in 2007 through HBAO's horizon search to GTAO's exact horizon-slice integral with temporal reuse — each generation trading cost for results closer to ray-traced ground truth.

1. The occlusion integral

Ambient occlusion at a surface point p with normal n is defined as the fraction of the hemisphere above p that is not blocked by nearby geometry, integrated with a cosine weight (same cosine term as the rendering equation, but with visibility V instead of full radiance):

AO(p) = 1 − (1/π) ∫_Ω V(p, ω) · (n·ω) dω

V(p, ω) = 0 if a nearby surface blocks direction ω within some radius r, else 1

AO ranges [0,1]: 1 = fully open, 0 = fully occluded (deep crevice)

A full ray-traced answer shoots hundreds of rays per pixel and tests scene intersection against every one, which is exactly what a path tracer already computes as a side effect of global illumination. Every real-time technique below is an approximation of this same integral under a tighter time budget — usually under 1ms per frame.

2. SSAO: random hemisphere sampling in depth space

Crytek's SSAO (Mittring, 2007) approximates the integral using only the depth buffer already available after the geometry pass — no extra scene traversal, no BVH. For each pixel, sample a handful of points in a hemisphere kernel around the surface point, reconstruct their view-space position, and compare against the depth buffer to guess whether they are occluded.

// SSAO fragment shader, simplified
vec3 pos    = reconstructViewPos(gl_FragCoord.xy, depthTex);
vec3 normal = texture(normalTex, uv).xyz;
vec3 tangent = normalize(texture(noiseTex, uv * noiseScale).xyz);
mat3 tbn = buildTBN(normal, tangent);

float occlusion = 0.0;
for (int i = 0; i < kernelSize; i++) {
  vec3 samplePos = pos + tbn * kernel[i] * radius;
  vec4 offset = projection * vec4(samplePos, 1.0);
  offset.xyz /= offset.w;
  offset.xyz = offset.xyz * 0.5 + 0.5;

  float sceneDepth = linearizeDepth(texture(depthTex, offset.xy).r);
  float rangeCheck = smoothstep(0.0, 1.0, radius / abs(pos.z - sceneDepth));
  occlusion += (sceneDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
}
float ao = 1.0 - (occlusion / float(kernelSize));

This is cheap but crude: it only knows about depth, not real geometry, so it can miss occluders behind the visible surface (screen-space incompleteness) and it needs 16–64 samples per pixel to avoid extreme noise.

3. Noise, blur, and the sample-count trade-off

With a small kernel, SSAO output is visibly noisy — random per-pixel rotation of the kernel (via a small tiling noise texture) trades structured banding for less objectionable high-frequency noise, which is then removed with a bilateral blur (depth/normal-aware, so it doesn't bleed AO across silhouette edges).

Kernel sizeRaw noiseTypical blur radiusCost
8–16 samplesVery noisy5×5–7×7Cheapest
16–32 samplesModerate4×4Balanced (most games)
64+ samplesLow3×3Expensive, rarely used raw

4. HBAO: horizon-based occlusion

Horizon-Based Ambient Occlusion (Bavoil, Sainz, Dimitrov — NVIDIA, 2008) reframes the problem geometrically instead of using arbitrary hemisphere samples: for each of several directions around the pixel in screen space, march outward along the depth buffer to find the "horizon angle" — the highest elevation angle at which nearby geometry blocks the hemisphere.

For direction φ: horizon angle h(φ) = max elevation angle of any sampled point along that direction

AO contribution from that direction ∝ sin(h(φ)) − sin(t(φ))

t(φ) = tangent-plane angle at p (accounts for surface tilt relative to the view)

Integrate over several φ directions (typically 4–8) → final AO

Because it marches along real depth-buffer samples rather than testing arbitrary offset points, HBAO captures thin occluders and contact shadows more faithfully than kernel-based SSAO, at a similar or slightly higher cost. HBAO+ (2013) added detail-preserving blur and multi-bounce color bleeding as refinements.

5. GTAO: ground-truth-based ambient occlusion

Ground Truth Ambient Occlusion (Jimenez et al., Activision, 2016) keeps HBAO's horizon-search structure but replaces its approximate cosine-weighted integral with the exact closed-form solution for the visibility integral restricted to a horizon-bounded arc — hence "ground truth": for a given horizon, this is the mathematically correct AO contribution, not a heuristic.

Exact per-slice integral (Jimenez 2016):

InnerIntegral(h1,h2,n) = ¼·(−cos(2h1−γ) + cosγ + 2h1·sinγ − cos(2h2−γ) + cosγ + 2h2·sinγ)

where γ is the angle between the projected normal and the slice plane

Result: visually near-identical to ray-traced AO with far fewer horizon samples than HBAO needed for the same quality

GTAO typically uses only 2 slice directions with several horizon steps each, combined with strong temporal accumulation (reusing previous frames' samples via reprojection) to reach convincing quality at a fraction of HBAO+'s per-frame sample count — this is the technique behind most modern game engines' default AO (Unreal, many custom engines).

6. Multi-bounce approximation and AO-lit color

Pure AO only darkens — it has no color, since it is meant to modulate ambient/indirect light rather than direct light. Real indirect light bounces off colored surfaces before returning, so naive AO applied to colorful scenes (say, a red room) looks too dark and grey in corners. Multi-bounce GTAO approximates this by feeding the base albedo back through a polynomial fit calibrated against reference path-traced multi-bounce GI, brightening and tinting the occlusion term instead of leaving it a flat grey multiplier.

Single-bounce AO

Simple grey multiply on ambient light. Fast, but crevices in colorful scenes look unnaturally dark.

Multi-bounce GTAO

Polynomial correction using surface albedo — corners tinted toward the bounced color, closer to reference GI.

Ray-traced AO (RTAO)

Real hardware ray queries against actual scene geometry, no screen-space limitation, but requires RT hardware.

7. Comparison and implementation notes

TechniqueYearCore ideaTypical cost
SSAO2007Random hemisphere kernel vs depth bufferLow, but noisy at low sample counts
HBAO2008Horizon search along depth bufferModerate, better geometric fidelity
HBAO+2013HBAO + detail-preserving blur, multi-bounce approx.Moderate–high
GTAO2016Exact horizon-slice integral + temporal reuseLow with TAA, high quality

For a WebGL2/WebGPU implementation, the practical pipeline is: depth + normal prepass → AO compute pass (SSAO kernel or GTAO horizon search) at half or full resolution → bilateral blur → multiply into ambient/indirect lighting term only (never into direct light or emissive). GTAO's temporal reuse needs motion vectors and a reprojection buffer, adding real implementation cost — a reasonable middle ground for a browser demo is HBAO-style horizon search without temporal accumulation, spatially denoised instead.

💡 Open Path Tracing Demo →

🔗 Related Simulations

💡Path Tracing ✳️Fractal