./jerome{m}

~/Notes/Shadow mapping.md · note · seedling · 5 min · updated

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 currentDepth to the shadow map depth at that texel.
  • If currentDepth > mapDepth + bias, the point is in shadow.
glsl
vec4 lightClip = lightVP * vec4(worldPos, 1.0);
vec3 projCoords = lightClip.xyz / lightClip.w;
projCoords = projCoords * 0.5 + 0.5;             // [-1,1] -> [0,1]
float shadowDepth = texture(shadowMap, projCoords.xy).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 high k.
  • 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 with glPolygonOffset / Vulkan depthBiasSlopeFactor.
  • 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 D32F everywhere 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