Ambient Occlusion: From SSAO to GTAO
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.
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 size | Raw noise | Typical blur radius | Cost |
|---|---|---|---|
| 8–16 samples | Very noisy | 5×5–7×7 | Cheapest |
| 16–32 samples | Moderate | 4×4 | Balanced (most games) |
| 64+ samples | Low | 3×3 | Expensive, 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
| Technique | Year | Core idea | Typical cost |
|---|---|---|---|
| SSAO | 2007 | Random hemisphere kernel vs depth buffer | Low, but noisy at low sample counts |
| HBAO | 2008 | Horizon search along depth buffer | Moderate, better geometric fidelity |
| HBAO+ | 2013 | HBAO + detail-preserving blur, multi-bounce approx. | Moderate–high |
| GTAO | 2016 | Exact horizon-slice integral + temporal reuse | Low 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.