Introduction

Look at the corner where two walls meet, or the gap between a coffee mug and a table. Even with no direct lamp pointing at it, that corner is darker. Not because a shadow falls there — because the sky can barely see it. Ambient occlusion is the algorithm that captures this effect.

The idea is one elegant integral: at every surface point, measure how much of the hemisphere above it is blocked by nearby geometry. A point on a flat open plain sees the whole sky and stays bright. A point wedged inside a tight crease sees almost none of it and goes dark. The result — without tracing a single light ray — is a contact shadow that makes flat-shaded geometry feel heavy and grounded.

Real-time graphics adopted a screen-space approximation called SSAO (Screen-Space Ambient Occlusion), introduced by Crytek in Crysis (2007). Instead of sampling the actual scene geometry, SSAO reads the depth buffer, fires a small hemisphere of random rays in screen space, and darkens pixels whose neighbors are closer to the camera. It runs in milliseconds per frame on a GPU and became one of the signature effects of the HD era.

Try It

The canvas below renders a bumpy heightfield top-down. Each pixel's brightness normally reflects only its height. Toggle Ambient Occlusion to add the AO pass: the algorithm fires sample rays around each pixel and darkens it proportionally to how many neighbors are higher — simulating how a valley can barely see the sky.

<!-- {{c_html_intro}} -->
<div class="controls">
  <label class="toggle-label">
    <input type="checkbox" id="aoToggle" checked>
    <span>{{lbl_ao_toggle}}</span>
  </label>
  <label class="slider-label" title="{{lbl_strength_title}}">
    {{lbl_strength}}: <input type="range" id="strengthSlider" min="0.2" max="2.0" step="0.1" value="1.0">
    <span id="strengthVal">1.0</span>
  </label>
  <label class="slider-label" title="{{lbl_samples_title}}">
    {{lbl_samples}}: <input type="range" id="samplesSlider" min="4" max="32" step="4" value="16">
    <span id="samplesVal">16</span>
  </label>
</div>
<canvas id="canvas"></canvas>
<p class="hint">{{lbl_hint}}</p>
/* {{c_css_intro}} */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: .6rem; }
.controls {
  display: flex; flex-wrap: wrap; gap: .5rem .9rem; align-items: center;
  margin-bottom: .5rem; font-size: .85rem;
}
.toggle-label { display: flex; align-items: center; gap: .4rem; cursor: pointer; font-weight: 600; color: #a8d8ea; }
.toggle-label input { width: 1.1rem; height: 1.1rem; accent-color: #a8d8ea; cursor: pointer; }
.slider-label { display: flex; align-items: center; gap: .35rem; color: #ccc; }
.slider-label input[type=range] { width: 90px; accent-color: #a8d8ea; cursor: pointer; }
.slider-label span { min-width: 1.8rem; text-align: right; font-variant-numeric: tabular-nums; color: #a8d8ea; }
canvas { display: block; width: 100%; max-width: 520px; height: 320px; border-radius: 8px; image-rendering: pixelated; }
.hint { font-size: .78rem; color: #888; margin-top: .45rem; line-height: 1.4; }
// Code not found

Notice how peaks and ridges stay bright while valleys and tight crevices go dark. The Strength slider controls how aggressively the darkening is applied; the Samples count trades quality for speed. Even with very few samples the effect is convincing because it only needs to approximate an integral — a rough Monte Carlo estimate is already visually compelling.

How It Works

The exact ambient occlusion at a surface point pp is the average visibility over its hemisphere:

AO(p)=1πΩV(p,ω)(n^ω)dωAO(p) = \frac{1}{\pi} \int_{\Omega} V(p, \omega)\, (\hat{n} \cdot \omega)\, d\omega

where V(p,ω)V(p, \omega) is 1 if the ray from pp in direction ω\omega hits nothing within a radius rr, and 0 otherwise; n^\hat{n} is the surface normal. In plain language: fire rays in every direction above the surface and count the fraction that escape without hitting anything.

  • Monte Carlo sampling: exact integration over the hemisphere is too expensive. Instead, draw kk random unit vectors in the upper hemisphere, cast rays, and average. With k=16k = 16 to 6464 samples the result is already visually smooth after a blur pass.
  • SSAO shortcuts: instead of tracing rays into the full scene, Crytek's variant samples points in a hemisphere of radius rr around pp in view space, reads their depth from the depth buffer, and counts how many are behind the surface. It is an approximation — it misses geometry outside the camera's view — but it runs in a single GPU pass.
  • Noise and blur: few samples mean visible noise. A separable Gaussian blur on the AO buffer (done in two passes: horizontal then vertical) removes the noise while preserving soft contact shadow boundaries. Most engines use a 4×44 \times 4 interleaved noise pattern plus a 4×44 \times 4 blur kernel to keep the combined cost low.
  • Bent normals: a refinement computes the mean unoccluded direction instead of just the scalar occlusion. Shading with the bent normal rather than the geometry normal further reduces the "detached shadow" look.

The algorithm's cost scales as O(kwh)O(k \cdot w \cdot h) per frame, where kk is the sample count, and w×hw \times h is the screen resolution — fully linear in the number of pixels, which is why it fits in a real-time budget. Related ideas appear in ray tracing and other visibility algorithms.

Where It Matters

Ambient occlusion punches far above its weight in perceived realism. It shows up in almost every modern rendering pipeline:

  • Game engines: Unity, Unreal Engine and Godot all ship SSAO or HBAO+ (Horizon-Based Ambient Occlusion, a higher-quality variant that traces along the depth buffer horizon). Even mobile games use a cheaper half-resolution AO pass.
  • Film and VFX: offline renderers like Arnold and RenderMan compute full ray-traced AO as part of their global illumination budget. It is often baked into texture maps ("AO maps") to avoid re-computing it at render time.
  • Product visualization and CAD: a quick AO bake in Blender or Maya immediately makes a mechanical part feel solid, even before any materials are assigned.
  • Architecture and urban simulation: AO tells architects which corners of a building will feel gloomy at any time of day — without running a full daylight simulation.
  • 3D scanning and point clouds: applying AO to LiDAR or photogrammetry data reveals surface details that flat shading hides.

The core idea — average local visibility — also appears in related algorithms like bent normals, light probes, and the radiosity method, and it is a conceptual cousin of the hemisphere sampling used in path tracing.

Conclusion

Ambient occlusion answers one deceptively simple question — how much of the sky can this point see? — and the answer alone is enough to darken every crease, settle every object onto its surface, and give flat-shaded geometry a sense of weight that no directional light can replicate.

The mathematics is just an integral over a hemisphere. The engineering trick is approximating it fast enough to run every frame: a handful of random ray samples, a depth-buffer shortcut, and a blur pass. Together they produce one of the highest visual-quality-per-millisecond effects in the rendering toolkit.

So the next time a corner looks unusually convincing, remember: no artist painted that shadow. An algorithm counted how many directions the sky was blocked, and the darkness followed from arithmetic alone.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/ambient-occlusion/Content licensed under CC BY-NC 4.0.