Shadow mapping

09-shadow-mapping.md · updated 2026-10-15

Render the scene from the light's point of view and keep only the depths. Then, while shading a pixel, ask: is anything closer to the light than I am? If yes, something is blocking the light and this pixel is in shadow. That is the entire technique, and every real implementation is a pile of fixes for the four ways it goes wrong.

The depth comparison

Transform the pixel into the light's clip space, then compare its depth against the depth stored at that position in the shadow map. The shadow pass is a cheap, depth-only draw — no materials, no lighting, often no pixel shader at all — but it is a second traversal of the scene, so your draw-call count doubles per shadow-casting light. The trouble is that the stored depth was sampled on the light's grid, not yours, so the two numbers never quite agree.

Acne, and the bias that fixes it

A surface shadows itself because one stored depth has to stand in for a whole texel's worth of surface. Half of that footprint ends up marginally farther from the light than the recorded value, so it fails its own test and you get stripes of false shadow. The fix is a bias: subtract a little before comparing.

A constant bias is not enough, because the error across a texel grows as the surface tilts away from the light. Slope-scaled bias scales it by the incidence angle; normal offset instead nudges the sample position along the surface normal, trading a depth error for a positional one.

Peter-panning

Too much bias and contact shadows detach — objects float above their own shadow. Tune the bias to the smallest value that kills the acne at your resolution, and lower it again whenever you raise that resolution. Rendering only back faces into the shadow map helps for closed, solid geometry.

Aliasing and blocky edges

One shadow texel covering many screen pixels means staircase edges, and no bias will fix it. The cure is texel density: raise the resolution, and fit the light's frustum tightly around what is actually visible. A directional light over a whole level has no single good frustum, so use cascades: several maps over nested slices of view distance, dense near the camera, coarse far away, picked per pixel by depth. An omnidirectional point light needs a cube map — six faces, six shadow passes.

Soft edges

PCF means averaging several neighbouring comparisons, not several depths. Compare first, filter after: that is what produces a fractional "in shadow" value. More taps and a wider spread give a softer edge at a fixed cost per pixel. PCSS goes further, widening the filter by the average blocker distance, so shadows harden at contact and blur with distance.

Demo

Shadow mapping in 2D — a 1D shadow map on a flat scene; drag the light, drop the resolution, and ride the bias slider from acne to peter-panning.

Gotchas

Symptom Cause
Stripey shadow patterns on lit surfacesdepth bias too small for the texel footprint
Objects float above their shadowsdepth bias too large (peter-panning)
Chunky staircase shadow edgestoo few texels per screen pixel; tighten the frustum or add cascades
Shadows pop or swim as the camera movesthe light frustum is refit each frame; snap it to texel increments
Objects stop casting when they leave the screenshadow frustum culled against the camera, not the light
Everything beyond a distance is litoutside the shadow frustum; clamp the lookup or fade out
Visible seams between cascadesno blend band over the cascade transition
Shadow cost scales badly with light counteach caster re-draws the scene; cap shadow-casting lights, cache static ones

Open the demo →

← all notes & demos