Tutorial · Intermediate · ~40 min
Three.js · Shadow Maps · PCF · VSM · CSM

Three.js Shadows — PCF, VSM, CSM & Tuning

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

// 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