Ambient occlusion

앰비언트 오클루전

Faking the natural darkening that happens where surfaces sit close together or fold inward, so ambient light can’t reach as easily.

Also known as: AOContact shadowSSAO
···
html
<span class="ao-cap">ambient occlusion · contact darkening</span>
css
.ao-cap{position:absolute;left:0;right:0;bottom:5%;text-align:center;font-size:clamp(9px,2.4vmin,12px);color:#dfe3ffcc}
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d0d14);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 1.5, 5.6);

const floor = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), new THREE.MeshStandardMaterial({ color: 0x2a2a3a, roughness: 0.9 }));
floor.rotation.x = -Math.PI / 2;
scene.add(floor);
const wall = new THREE.Mesh(new THREE.PlaneGeometry(8, 4), new THREE.MeshStandardMaterial({ color: 0x232332, roughness: 0.9 }));
wall.position.set(0, 2, -2); scene.add(wall);

const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.1, 48, 48), new THREE.MeshStandardMaterial({ color: 0xffb648, roughness: 0.6 }));
sphere.position.set(-0.2, 1.1, -0.8);
scene.add(sphere);
scene.add(new THREE.HemisphereLight(0xffffff, 0x1a1a22, 1.1));

function makeAoTex() {
  const c = document.createElement('canvas'); c.width = c.height = 128;
  const g = c.getContext('2d');
  const rad = g.createRadialGradient(64, 64, 4, 64, 64, 64);
  rad.addColorStop(0, 'rgba(0,0,0,0.75)'); rad.addColorStop(1, 'rgba(0,0,0,0)');
  g.fillStyle = rad; g.fillRect(0, 0, 128, 128);
  return new THREE.CanvasTexture(c);
}
const aoTex = makeAoTex();
const aoFloor = new THREE.Mesh(new THREE.CircleGeometry(1.6, 32), new THREE.MeshBasicMaterial({ map: aoTex, transparent: true, depthWrite: false }));
aoFloor.rotation.x = -Math.PI / 2;
aoFloor.position.set(-0.2, 0.01, -0.6);
scene.add(aoFloor);
const aoWall = aoFloor.clone();
aoWall.rotation.x = 0;
aoWall.position.set(-0.2, 1.6, -1.98);
scene.add(aoWall);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const cycle = (Math.sin(t * 0.0007) + 1) / 2;
  aoFloor.material.opacity = cycle;
  aoWall.material.opacity = cycle;
  sphere.rotation.y = t * 0.0003;
  camera.lookAt(-0.1, 0.9, -0.6);
  renderer.render(scene, camera);
});

Direct light alone — the sun, a lamp — doesn’t explain why corners, crevices, and places where two surfaces meet look darker. That’s indirect light: ambient bouncing in from every direction, which a tight gap simply can’t receive as much of. AO precomputes “how occluded is this point” for every point on a surface, then nudges corners, folds, and contact points a little darker.

Real-time rendering approximates this per frame from camera depth — screen-space ambient occlusion (SSAO) — or, for a static scene, bakes each surface point’s occlusion into a texture ahead of time. Either way it’s an approximation, not an actual light simulation.

AO isn’t a shadow by itself — it’s closer to the ingredient that makes a “contact shadow” read as real, the thing that makes an object look like it’s actually sitting on the ground instead of floating just above it. Blender, Maya, and C4D all let you render AO as its own pass so its strength can be tuned separately in compositing.

When to use

Reach for it when an object looks like it’s floating above the floor or another surface and needs a grounded, contact-point weight.