Command Palette

Search for a command to run...

Hectal
Course outline
Topic 5.5Intermediate ~24 min · step 5 of 7

Shadows

In one line

A shadow is a second picture, drawn from the light's point of view, that stores how far the nearest surface is. Then every pixel asks: am I farther than that? You learn the five switches that must all be on, and fix the three classic faults: no shadow, a cut-off shadow and acne.

0/7 · 0%

Think of it like this

Stand where the lamp is and look at the room. Everything you can see is lit; everything hidden behind something is in shadow. Write down, for each direction, how far the nearest thing is. To check whether a spot on the floor is shadowed, compare its distance from the lamp with your list. That list is a shadow map.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

shadow map
A texture, drawn from the light's point of view, that stores the distance to the nearest surface in each direction.
shadow camera
The box or cone of space a light's shadow map covers. Objects outside it cast no shadow.
acne
Stripes of false shadow on a surface that shadows itself because of limited depth precision.
bias
A small shift of the stored depth that stops acne. Too much makes shadows float.

Step by step

01Set up a shadow in five lines

The first example turns on the renderer's shadow map, the light, the ball (caster) and the floor (receiver). It also sizes the shadow camera to ±2 m.

02Find where the shadow falls

The code follows the light's ray from the ball to the floor to find the middle of the shadow and reads that pixel. A lit floor pixel far from the ball reads 219; the shadow reads 101.

03Switch things off one by one

Each row turns one thing off, reads the pixel, and turns it back on. Every row after the first prints 219 NO SHADOW: all five switches matter, and so does a shadow camera that is too small (±0.05).

04A gotcha: toggling the renderer switch

renderer.shadowMap.enabled is baked into the compiled materials. After the first frame, changing it does nothing until the materials are rebuilt with material.needsUpdate = true (the example does this). Without it the old shadow map is still read, and the shadow stays.

Try it yourself

  1. 1

    Make the shadow maps bigger

    In the third example change 1024 to 2048. The memory column prints 16 MiB, 16 MiB and 96 MiB: four times the texels per map, and six maps for the point light.

  2. 2

    Make the shadow camera tight

    In the first example change the four sun.shadow.camera lines (left, right, top, bottom) from -2 and 2 to -0.05 and 0.05. The first row, all five on, now prints 219 NO SHADOW too. Look at the picture: the ball is almost outside the box, so only a tiny square of shadow is left.

  3. 3

    Make the shadow map tiny

    In the first example add sun.shadow.mapSize.set(16, 16); after sun.castShadow = true;. The ball-shaped shadow becomes a soft, wide smudge with faint fine lines at its edge (seen in the checker's software renderer). A 4 m box over 16 texels gives 25 cm per texel, too coarse for a 0.8 m ball. The table is unchanged.

Code & diagrams

The five switches and a too-small shadow camera New tab

The picture is the working setup: a ball, its shadow and a floor. The table is a table of pixel readings, not of what you see.

idle

Console

Run the scene to see its console.

Expected console output

lit floor, far from the ball: 219
all five on                        pixel in the shadow's centre: 101 shadow
renderer.shadowMap.enabled = false pixel in the shadow's centre: 219 NO SHADOW
light.castShadow = false           pixel in the shadow's centre: 219 NO SHADOW
ball.castShadow = false            pixel in the shadow's centre: 219 NO SHADOW
floor.receiveShadow = false        pixel in the shadow's centre: 219 NO SHADOW
shadow camera too small (±0.05)    pixel in the shadow's centre: 219 NO SHADOW
Acne: bias and normalBias on a skimming light New tab

A double-sided floor that casts and receives shadow, a low light and a small shadow map: stripes on the first run, then the numbers fall. The measured block is 100 × 40 pixels of a 300 × 150 frame.

idle

Console

Run the scene to see its console.

Expected console output

wrongly dark pixels, out of 4000:
bias 0, normalBias 0 (the default)  3918
bias -0.005                         2148
bias -0.02                             0
normalBias 0.05                       45
bias -0.0005 and normalBias 0.05       9
The cost of a shadow: passes and memory New tab

The script counts how many times three.js draws into a shadow map. The point light does it six times. Draw calls stay 3 because the ball is in only one face of the cube.

idle

Console

Run the scene to see its console.

Expected console output

light            shadow passes  draw calls  memory (about 4 bytes a texel)
DirectionalLight         1           3      4 MiB
SpotLight                1           3      4 MiB
PointLight               6           3     24 MiB

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Take the biases away

In the acne example remove the sun.shadow.bias and sun.shadow.normalBias lines (or set both to 0).

terminal
$ Run
── what you'll see ──
bias 0, normalBias 0 (the default) 3918
3918 of the 4000 pixels in the block are dark although the sun shines on them: the picture is covered with stripes.

Myth vs fact

Myth

If there is no shadow, three.js is broken.

Fact

Almost always one of the five switches is off, or the object is outside the shadow camera. Go through them in order, as the first example does.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    In src/renderers/webgl/WebGLShadowMap.js, a point light's map is a WebGLCubeRenderTarget and the render loop runs faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : .... Each face has its own 90° camera along ±x, ±y, ±z. The example counts the six setRenderTarget calls.

  • ▸

    For closed meshes with FrontSide materials, three.js draws the back faces into the shadow map (shadowSide), which avoids most acne for free. That is why acne showed in the example only with a DoubleSide floor. Setting material.shadowSide changes this.

Remember this

  1. 1

    The problem this solves: a bike without a shadow floats; a shadow shows where it touches the ground. But shadows fail most often in three.js, usually from one missing switch and with no error message.

  2. 2

    The method. Before the main picture, three.js renders the scene again from each shadow-casting light, into a texture holding only depth. This is the shadow map. In the main picture each pixel is moved into the light's view, and compared with the stored depth. If it is farther, something is in front of it: it is in shadow.

  3. 3

    Five switches, all on. renderer.shadowMap.enabled = true; light.castShadow = true; mesh.castShadow = true on the object that makes the shadow; mesh.receiveShadow = true on the surface that shows it; and the object must be inside the light's shadow camera, the box (for a directional light) or cone of space the shadow map covers.

  4. 4

    A cut-off shadow. The shadow camera of a DirectionalLight is a box (left, right, top, bottom, near, far) around the target. Things outside it cast no shadow. Make it as small as the scene allows: a small box also makes every shadow texel smaller, and a sharper shadow. A blocky or smudged shadow means shadow.mapSize is too small for the box.

  5. 5

    Acne and bias. The depth map has limited precision, so a surface can find itself slightly 'behind' its own stored depth and shadow itself, in stripes called acne. shadow.bias moves the stored depth by a small number; shadow.normalBias pushes the lookup out along the surface normal by a distance in metres. Too much of either makes the shadow float away from the object (peter-panning) or lose its thin parts.

  6. 6

    Cost. A shadow-casting light draws the casting meshes an extra time and needs a texture: a 1024 × 1024 map is about 4 MiB. A PointLight shines in all directions, so three.js renders six maps (a cube): six passes, six times the memory. The third example prints it. A directional or spot light needs one.

  7. 7

    Soft edges. renderer.shadowMap.type is PCFShadowMap by default: each pixel averages several nearby depth tests, so edges are soft. BasicShadowMap is hard-edged and faster. VSMShadowMap is softer and can leak; point lights are not supported (three.js warns and skips them). PCFSoftShadowMap is gone in r186 and falls back to PCF with a warning.

Explain it without notes

01

A mesh casts no shadow. Give the five checks in the order you would do them.

02

Why does a point light cost six times the memory of a directional light at the same mapSize?

Practice

01

A shadow looks blocky. Name two fixes and the cost of each.

02

Write the lines for a directional key light at (2, 4, 3) that shadows a scene about 3 m across.

Done when you can

  • Name the five switches and check them in order

  • Size a directional light's shadow camera to a scene

  • Fix acne with normalBias and bias, and say what too much does

  • Say why a point light renders six shadow maps and what it costs