Introduction

Open any 3D game, pause on a bright scene, and look at the shadows. Every one of them — the sharp silhouette of a building, the soft penumbra under a tree — was calculated in a fraction of a millisecond by the same elegant trick invented by Lance Williams in 1978: shadow mapping.

The core idea is almost childishly simple. Before drawing the scene for your eye, the GPU draws it once more, pretending to be the light. From that vantage point it records only how far away each surface is — a depth value per pixel called the shadow map. Then, when drawing the scene for real, it asks for every visible point: was the light closer than that depth value, or farther? Closer means lit; farther means something blocked the light — shadow.

That depth-comparison is O(1)O(1) per pixel — a single texture lookup and one numerical test. Yet from this nearly trivial operation emerges the convincing shadows that make 3D graphics feel three-dimensional. Understanding shadow mapping means understanding the bridge between simple geometry and believable light.

Move the Light

The demo below runs a 2D version of the full algorithm. There is one point light, two rectangular blockers, and a receiver plane. Drag the light to move it and watch the shadows swing.

<!-- {{c_html_desc}} -->
<div class="hint">{{hint_para}}</div>
<div class="scene-wrap">
  <canvas id="scene" width="420" height="320"></canvas>
  <div class="coords" id="coords"></div>
</div>
<div class="controls">
  <label>{{label_light_x}} <input type="range" id="lx" min="60" max="360" value="210"></label>
  <label>{{label_light_y}} <input type="range" id="ly" min="20" max="140" value="50"></label>
</div>
<div class="status" id="status">{{status_drag}}</div>
<div class="legend">
  <span class="leg-lit">&#9632; {{leg_lit}}</span>
  <span class="leg-shadow">&#9632; {{leg_shadow}}</span>
  <span class="leg-blocker">&#9632; {{leg_blocker}}</span>
  <span class="leg-light">&#9711; {{leg_light}}</span>
</div>
/* {{c_css_desc}} */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
.scene-wrap { position: relative; display: inline-block; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #0a0f1a; }
.coords { position: absolute; top: 6px; right: 8px; font-size: .75rem; color: #aaa; pointer-events: none; }
.controls { display: flex; flex-direction: column; gap: .35rem; margin: .5rem 0; }
label { font-size: .85rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { flex: 1; }
.status { font-size: .9rem; font-weight: 600; color: #1d3557; min-height: 1.3em; margin: .3rem 0; }
.legend { display: flex; flex-wrap: wrap; gap: .6rem; font-size: .78rem; margin-top: .3rem; }
.leg-lit { color: #f4c842; }
.leg-shadow { color: #2d3d5a; }
.leg-blocker { color: #7a5c2e; }
.leg-light { color: #ffe680; }
// Code not found

Notice what happens step by step: the algorithm first traces rays from the light to record the nearest distance it can see along each angle (the shadow map), then checks every surface point against that recorded distance. Points the light cannot reach directly — because a blocker is closer — fall into shadow. The comparison is instantaneous; the tricky part is storing and sampling the depth values accurately enough to avoid the shimmering artifact known as shadow acne.

The Real Complexity

Shadow mapping is deceptively cheap on paper and surprisingly finicky in practice.

Why it is fast. The shadow-map pass renders the scene once from the light, writing a depth value per texel — O(T)O(T) work where TT is the shadow-map resolution. The shading pass then performs one texture lookup per screen pixel — O(P)O(P) work. Total cost is O(T+P)O(T + P), linear in pixels, with no rays cast at all.

Why it is tricky. Three problems conspire to ruin naĂŻve shadow mapping:

  • Shadow acne. A surface can shadow itself when the depth stored in the shadow map is slightly off due to the finite resolution of floating-point numbers. The fix is to add a small bias — shift the stored depth by Δ\varepsilon — but too large a bias causes light to leak underneath objects (Peter Panning).
  • Aliasing. A single shadow-map texel covers many screen pixels near the camera, producing jagged shadow edges. Cascaded Shadow Maps (CSM) split the view frustum into slices, using a high-resolution map close to the camera and coarser maps far away.
  • Hard vs. soft edges. Real light sources are not points, so shadows have a penumbra. Percentage Closer Filtering (PCF) averages several shadow-map samples to blur edges. PCSS (Percentage Closer Soft Shadows) varies the blur radius with estimated blocker distance to mimic area-light penumbrae.

These refinements do not change the asymptotic cost but multiply the constant: a production game renderer may issue 16 or more shadow-map samples per pixel per light. The fundamental insight — compare depth from the light's view — remains unchanged.

Where It Matters

Shadow mapping is not one algorithm among many — it is the default choice whenever shadows must be rendered at interactive frame rates:

  • Game engines. Unreal Engine, Unity, and Godot all ship cascaded shadow maps as their primary shadow method. The cascade count, resolution, and bias values are the most-tuned parameters in any outdoor scene.
  • Augmented and mixed reality. AR overlays virtual objects on camera feeds. For the overlay to look convincing, virtual shadows must fall on real surfaces, requiring a depth map of the real scene — the same depth-comparison logic applied to a captured depth buffer.
  • Film VFX previsualization. Feature-film rendering uses path tracing for final images, but directors and artists review shots in real time using shadow-mapped previews that are physically convincing enough for editorial decisions.
  • Architectural visualization. Interactive walkthroughs of buildings render thousands of shadow-casting lights with techniques derived from shadow mapping, including virtual shadow maps that tile the entire scene into one giant depth atlas.

The same depth-comparison idea also powers screen-space ambient occlusion (SSAO), which approximates how much ambient light reaches each surface point — another case where a single depth buffer unlocks a whole class of lighting effects. See also ray tracing for the complementary approach where every shadow ray is explicitly traced.

Conclusion

Shadow mapping distills an apparently difficult question — "is this point in shadow?" — into a single depth lookup. Render from the light, record distances, compare: three steps, and the scene fills with shadows that make it feel real.

The practical complications — acne, aliasing, soft penumbrae — are real, but they all live in the constant factor, not in the algorithm's fundamental cost. The insight that a change of viewpoint (from the camera to the light) can turn a hard visibility query into a trivial comparison is as elegant as anything in computer graphics.

Next time you see a crisp shadow stretching across a game world, remember: the GPU rendered that scene twice, once with the light's eyes, and the second time it just looked up a number.

Share this article

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

Comments

Loading comments...

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