Three.js implements shadow mapping: the scene is rendered from the
light's point of view into a depth texture, which is then compared
during the main render pass. This guide covers enabling shadows,
choosing shadow types, eliminating shadow acne, and scaling to large
scenes with cascaded shadow maps.
1Enable shadows on renderer, lights, and
objects
Shadow rendering requires three opt-in flags — all three must be set:
// 1. Renderer renderer.shadowMap.enabled = true; // 2. Each light
that should CAST shadows const sun = new
THREE.DirectionalLight(0xffffff, 2); sun.castShadow = true;
scene.add(sun); // 3. Each mesh that CASTS or RECEIVES shadows
mesh.castShadow = true; // this object throws a shadow
ground.receiveShadow = true; // other objects' shadows are visible on
this // Objects can both cast AND receive simultaneously
mesh.castShadow = true; mesh.receiveShadow = true;
Shadow-casting lights supported by Three.js:
DirectionalLight, SpotLight, and
PointLight. AmbientLight and
HemisphereLight do not cast shadows — they have no single
direction.
2Shadow map types — BasicShadowMap to
VSM
Type
Quality
Cost
Best for
BasicShadowMap
Hard, aliased
Lowest
Debugging / stylised
PCFShadowMap
Soft PCF filter
Low
Default; most games
PCFSoftShadowMap
Softer, wider
Medium
Architectural viz
VSMShadowMap
Very soft, bilinear
Medium+
Open landscapes
import * as THREE from 'three'; renderer.shadowMap.type =
THREE.PCFSoftShadowMap; // default is PCFShadowMap // VSM uses a depth
+ depth² texture so it can cache softness via mipmaps:
renderer.shadowMap.type = THREE.VSMShadowMap; // VSM Note: also set
light.shadow.blurSamples for better quality sun.shadow.blurSamples =
25;
3Tuning the shadow camera frustum
A DirectionalLight uses an orthographic shadow camera.
Its frustum must be as tight as possible around the
visible scene for best shadow texel density:
sun.shadow.camera.near = 0.5; sun.shadow.camera.far = 100;
sun.shadow.camera.left = -20; sun.shadow.camera.right = 20;
sun.shadow.camera.top = 20; sun.shadow.camera.bottom = -20;
sun.shadow.camera.updateProjectionMatrix(); // Increase texels for
detail sun.shadow.mapSize.set(2048, 2048); // default is 512×512 //
Powers of 2 only: 512, 1024, 2048, 4096 // Visualise the camera
frustum while tuning import { CameraHelper } from 'three';
scene.add(new CameraHelper(sun.shadow.camera));
A frustum twice as large halves the shadow texel density. Prefer a
tight fit over a large mapSize — texture memory is
limited and a 4096² map uses 64× more memory than a 512² one.
4Eliminating shadow acne with bias
Shadow acne (self-shadowing stripes) appears because
depth comparison precision is limited. Fix it with
bias and normalBias:
// bias offsets the shadow map depth — push it away from the surface
sun.shadow.bias = -0.0001; // negative moves shadow toward the light
// normalBias offsets along the surface normal — better for curved
surfaces sun.shadow.normalBias = 0.05; // Typical starting values: //
Flat surfaces: bias = -0.0001, normalBias = 0 // Curved meshes: bias =
-0.00005, normalBias = 0.02..0.05 // Peter Panning (shadow detaches
from base) means bias is too large // → reduce until shadow rejoins
the object
5Cascaded Shadow Maps (CSM) for large
scenes
CSM splits the view frustum into near/mid/far cascades, each with its
own shadow map. Near objects get more texels; distant objects fewer.
Available via three/examples/jsm:
import { CSM } from 'three/examples/jsm/csm/CSM.js'; const csm = new
CSM({ maxFar: camera.far, cascades: 4, // 1–8 typically shadowMapSize:
1024, lightDirection: new THREE.Vector3(1, -1, 1).normalize(), camera:
camera, parent: scene, mode: 'practical', // 'uniform' | 'logarithmic'
| 'practical' lightIntensity: 1.5, fade: true, // smooth transition
between cascades }); // Update every frame (CSM follows the camera)
renderer.setAnimationLoop(() => { csm.update(); // recompute cascade
splits csm.updateFrustums(); // for moving objects / camera
renderer.render(scene, camera); }); // Materials must opt into CSM
shader injection: csm.setupMaterial(material); // adds defines for CSM
cascade selection
CSM is essential for open-world or outdoor scenes. Without it a single
shadow map must cover kilometres of terrain and produces very low
shadow resolution near the camera.
6Performance tips
Freeze static shadows — set
renderer.shadowMap.autoUpdate = false and call
renderer.shadowMap.needsUpdate = true only when objects
move.
Limit active shadow lights — each shadow light is a
full GPU scene render. Keep it to 1–2 at most unless using
mobile-optimised techniques.
Use smaller mapSize for point lights — a
PointLight renders 6 shadow maps (one per cube face).
Even 512×512 = 6 shadow render passes.
Disable shadows on small/distant objects — set
obj.castShadow = false for objects that never cast a
visible shadow.
Half-float VSM — Three.js uses RGB packing for VSM
by default. Enable
renderer.shadowMap.type = THREE.VSMShadowMap only if
you need very smooth, blurrable shadows.
// Static scene example — only re-render shadows on demand
renderer.shadowMap.autoUpdate = false; function makeDirty() {
renderer.shadowMap.needsUpdate = true; // triggers one shadow
re-render } // Call makeDirty() only when objects actually move