Shadow mapping
// status: seedling
Shadow mapping is the classic real-time shadow technique: render the scene from the light's point of view, keep only depth, then at shading time re-project each pixel into the light's clip space and compare depths. It's been in every real engine since the late 90s and its failure modes are practically a rite of passage.
The two passes
Pass 1 — from the light:
- Build a view matrix from the light (directional: orthographic; point: six cube faces; spot: perspective with cone angle).
- Render only depth into a depth texture (
DEPTH_COMPONENT*).
Pass 2 — during shading:
- Transform the shaded point into the light's clip space.
- Compare
currentDepthto the shadow map depth at that texel. - If
currentDepth > mapDepth + bias, the point is in shadow.
vec4 lightClip = lightVP * vec4;
vec3 projCoords = lightClip.xyz / lightClip.w;
projCoords = projCoords * 0.5 + 0.5; // [-1,1] -> [0,1]
float shadowDepth = .r;
float shadow = projCoords.z - bias > shadowDepth ? 0.0 : 1.0;This is simple and wrong in interesting ways.
The classic artefacts
Every one has a name and a fix:
- Shadow acne — diagonal-stripe self-shadowing on lit surfaces. Cause: depth resolution at glancing angles. Fix: a small bias or, better, a slope-scale bias that scales with surface angle. Better still: render the shadow map with front-face culling so you sample the far side.
- Peter-panning — objects float above their shadows. Cause: too much bias. The trade-off with acne is eternal; slope-scale bias + front-face culling lets you use a much smaller constant bias.
- Perspective aliasing — near objects get a handful of shadow texels, far objects barely any. Fix: Cascaded Shadow Maps (below) for directional lights.
- Projective aliasing — surfaces parallel to the light direction have horrible shadow resolution. Fix: mostly live with it; soft filtering hides a lot.
- Hard edges — real shadows have penumbras; shadow maps are a boolean per-texel. Fix: filtering (PCF, VSM, PCSS) or ray traced shadows.
Cascaded Shadow Maps (CSM)
For directional lights covering a large view, you split the camera frustum into N slices (typically 3–4), fit a tight ortho projection to each slice, and render a shadow map per slice. At shade time, pick the cascade containing the pixel's view-space depth.
Gets you near-pixel-precision shadows near the camera and cheap-but-blurry shadows far away. Pitfalls:
- Cascade seams — switching from cascade N to N+1 shows a line. Fix: blend a few pixels across the boundary.
- Stable projection — as the camera moves, cascade bounds wobble, making the shadow map contents shimmer. Fix: snap the ortho projection to the texel grid ("Sample Distribution Shadow Maps" or simpler texel-snap).
- Counting cascades is expensive — keep N small; 3 is usually enough with good filtering.
Point and spot lights
- Spot — one shadow map, perspective projection along the cone axis. Same as directional but perspective.
- Point (omni) — six shadow maps (a cube), or a dual-paraboloid map. Cube is standard; paraboloid is cheaper but has edge artefacts.
- Many point lights — a single atlas texture with tiles per light, plus a lookup to find each light's tile.
Filtering — from hard to soft
From cheap to expensive:
- Percentage-Closer Filtering (PCF) — sample a small kernel (e.g. 3×3 or Poisson disk) around the projected coord, average the 0/1 shadow results. Hardware PCF (
sampler2DShadow) does a 2×2 bilinear-filtered depth comparison for free. - Random rotated PCF — rotate the kernel per pixel by a hash to turn banding into noise.
- Variance Shadow Maps (VSM) — store depth + depth²; compute a Chebyshev upper bound. Nice soft shadows, but suffer from light bleeding near occluders.
- Exponential Shadow Maps (ESM) — store
exp(k·depth); works nicely with summed-area or standard filtering. Also bleeds at highk. - Moment Shadow Maps — 4-moment generalisation of VSM, better quality at higher memory cost.
- Percentage-Closer Soft Shadows (PCSS) — kernel size scales with blocker distance. Genuinely physically motivated penumbra. Expensive.
- Contact-hardening + screen-space — fake PCSS on the far pixels, real PCSS near contact. Nvidia HFTS and similar.
Beyond shadow maps
Alternatives worth knowing:
- Ray traced shadows — on hardware that supports it (RTX, DXR, VK_KHR_ray_tracing), cast a shadow ray per pixel per light. Denoise. Beautiful results, not cheap.
- Signed distance field shadows — if you have an SDF of the scene (see Ray marching and SDFs), soft shadows fall out of the march almost for free.
- Screen-space shadows — short-range ray march in screen depth, great for contact shadows filling in where the shadow map is too coarse.
Biasing, properly
A couple of tricks that together dissolve most acne/peter-panning:
- Slope-scale bias —
bias = constant + slopeScale * tan(acos(dot(N, L)))— the driver can apply this withglPolygonOffset/ VulkandepthBiasSlopeFactor. - Normal offset — before projecting into the shadow map, push the shaded point along the surface normal by a small distance scaled by
1 - NoL. The shadow "floats" a bit but acne goes away. - Front-face culling in the shadow pass — only back faces write depth. Self-shadowing uses the far side, which is less prone to z-fighting.
Combine all three. Tune on the worst-case surface angle you'll see.
Things that tripped me up
- Shadow map format — 16-bit depth aliases hard at long distances. Use
D32Feverywhere you can afford it. - Texel snapping in CSM — without it, shadows shimmer. With it, they're rock solid.
- Seam between cascades — always noticeable at first and always fixable with a small overlap/blend.
- Alpha-tested geometry — leaves, grids, etc. The shadow pass needs to discard the same way the main pass does, or you get solid-block shadows from leaves.
sampler2DShadow≠sampler2D— the hardware PCF path needs the depth texture bound as a comparison sampler. Easy to miss the bind change.- Orthographic frustum fitting — too tight and shadows cull off-screen; too loose and resolution craters. Fit to the light-space AABB of the cascade's world-space corners.
References
- Real-Time Shadow Algorithms — Eisemann et al., still the best textbook.
- Shadow Mapping: GPU-based Tips and Techniques — GPU Gems 1 chapter, canonical.
- Sampling Distribution Shadow Maps — Lauritzen et al.
- Percentage-Closer Soft Shadows — Fernando (NVIDIA) 2005.
Back to Notes · see also Physically Based Rendering · Deferred vs forward rendering