Three.js Shadows: Setup and Fixing Common Problems

Three.js shadows need three switches set together: renderer.shadowMap.enabled = true on the renderer, light.castShadow = true on the light, and mesh.castShadow / mesh.receiveShadow on the relevant objects. After that, most shadow problems (acne, peter-panning, hard clipped edges) come down to tuning the shadow camera's frustum and the light's shadow.bias.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The three switches, again

This trips up almost everyone at least once, so it's worth stating precisely:

  1. renderer.shadowMap.enabled = true. Without this, no shadow map is ever rendered, full stop, no matter what else is configured.
  2. light.castShadow = true. Only DirectionalLight, PointLight and SpotLight support shadows; AmbientLight and HemisphereLight never cast them.
  3. Per-mesh: mesh.castShadow = true on anything that should cast a shadow, mesh.receiveShadow = true on anything that should show shadows cast onto it (commonly a ground plane, which usually wants receiveShadow but not castShadow).

Set renderer.shadowMap.type to THREE.PCFSoftShadowMap for softer edges than the default: a cheap visual upgrade.

The shadow camera frustum: the #1 cause of missing shadows

A DirectionalLight's shadow is rendered from an orthographic camera whose frustum you must size to fit your scene: light.shadow.camera.{left,right,top,bottom,near,far}. The default frustum is small (roughly a 10-unit box), so in a larger scene, objects and their shadows simply fall outside it and don't render, which looks exactly like "shadows aren't working" but is actually clipping.

Size the frustum to comfortably cover your scene's extent, but not much larger. An oversized frustum spreads the same shadow-map resolution over a bigger area, making shadows blurrier and more prone to acne (see below). For SpotLight and PointLight, the shadow camera is perspective and tied to the light's own angle/range, so there's less manual tuning needed.

Fixing shadow acne and peter-panning

Two artifacts show up constantly:

  • Shadow acne: a moiré/striped self-shadowing pattern on lit surfaces, caused by shadow-map precision limits. Fix by increasing light.shadow.bias (a small negative offset, e.g. -0.001) or light.shadow.normalBias, which is usually the more predictable of the two to tune.
  • Peter-panning: the shadow appears detached from the object, floating slightly away from its base. Usually caused by overcorrecting bias to fix acne. It's a genuine tradeoff: nudge bias values incrementally rather than jumping to a large offset.
  • Blocky/low-res shadow edges: increase light.shadow.mapSize.width/height (default is often 512; try 1024 or 2048), and tighten the shadow camera frustum to the actual scene bounds rather than leaving it oversized.

Tools and libraries

Code

Full shadow setup (r181)
1import * as THREE from 'three'
2
3renderer.shadowMap.enabled = true
4renderer.shadowMap.type = THREE.PCFSoftShadowMap
5
6const light = new THREE.DirectionalLight(0xffffff, 3)
7light.position.set(5, 10, 5)
8light.castShadow = true
9light.shadow.mapSize.set(2048, 2048)
10light.shadow.camera.left = -8
11light.shadow.camera.right = 8
12light.shadow.camera.top = 8
13light.shadow.camera.bottom = -8
14light.shadow.camera.near = 1
15light.shadow.camera.far = 30
16light.shadow.normalBias = 0.02 // fixes acne without visible peter-panning
17scene.add(light)
18
19mesh.castShadow = true
20ground.receiveShadow = true
Shadows are renderer-level, not material-level
1// Shadow mapping is configured on the renderer and lights, not the
2// material. TSL node materials receive and cast shadows the same way
3// MeshStandardMaterial does, no extra node wiring required.
4renderer.shadowMap.enabled = true
5light.castShadow = true
6mesh.material = new THREE.MeshStandardNodeMaterial() // shadows work unchanged

Learn this properly

Learn Practical TSL

Your First Node Material

Get a scene and material working first, then layer shadows on top.

Start the lesson (6 minutes)

Frequently asked questions

Why aren't shadows showing up in my Three.js scene?

Check all three required switches: renderer.shadowMap.enabled, light.castShadow on the light, and castShadow/receiveShadow on the relevant meshes. If those are all set, check whether the shadow camera frustum (for DirectionalLight) is actually covering your scene. Objects outside it won't cast a visible shadow.

How do I fix shadow acne in Three.js?

Increase light.shadow.normalBias (try small values like 0.02) or light.shadow.bias (small negative values like -0.001). Increase gradually: overcorrecting causes the opposite problem, peter-panning, where the shadow detaches from its object.

Which shadow map type looks best in Three.js?

THREE.PCFSoftShadowMap gives noticeably softer, more natural shadow edges than the default THREE.PCFShadowMap, for a modest performance cost. Set it via renderer.shadowMap.type.

Keep reading