iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Shadows

Shadows in three.js use shadow maps: lights project a depth texture, meshes that "receive shadows" sample it. Enable on the renderer, the light, the casters, and the receivers. Tune map size + bias to balance quality and performance.

Enable shadows, tune bias, cascade for big scenes

EXAMPLE
import * as THREE from 'three';

// 1) Enable on renderer
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.shadowMap.type    = THREE.PCFSoftShadowMap;     // softer edges
// Other options: BasicShadowMap (fast, harsh), VSMShadowMap (variance)

// 2) Light that CASTS shadows
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
sun.position.set(5, 10, 4);
sun.castShadow = true;

// Shadow map size — bigger = sharper but slower
sun.shadow.mapSize.set(2048, 2048);

// Shadow camera — defines the frustum that gets shadow-mapped
sun.shadow.camera.left   = -10;
sun.shadow.camera.right  =  10;
sun.shadow.camera.top    =  10;
sun.shadow.camera.bottom = -10;
sun.shadow.camera.near   = 0.5;
sun.shadow.camera.far    = 50;

// Bias — slight offset to avoid shadow acne
sun.shadow.bias       = -0.0005;
sun.shadow.normalBias =  0.02;

// Radius — soft penumbra (with PCFSoftShadowMap)
sun.shadow.radius = 4;

scene.add(sun);

// Optional: visualise the shadow camera
const helper = new THREE.CameraHelper(sun.shadow.camera);
scene.add(helper);

// 3) Meshes that CAST + RECEIVE shadows
const ground = new THREE.Mesh(
  new THREE.PlaneGeometry(20, 20),
  new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 1 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

const box = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 0x224488, roughness: 0.5 })
);
box.position.y = 0.5;
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);

// For GLB / GLTF models: traverse + set on every mesh
const loader = new GLTFLoader();
loader.load('/model.glb', (gltf) => {
  gltf.scene.traverse((n) => {
    if (n.isMesh) {
      n.castShadow = true;
      n.receiveShadow = true;
    }
  });
  scene.add(gltf.scene);
});

// 4) Spot lights
const spot = new THREE.SpotLight(0xffffff, 2);
spot.position.set(2, 4, 3);
spot.castShadow = true;
spot.shadow.mapSize.set(1024, 1024);
spot.shadow.camera.near = 0.5;
spot.shadow.camera.far  = 20;
scene.add(spot);

// 5) Point lights (omnidirectional shadows)
const point = new THREE.PointLight(0xffeecc, 1);
point.position.set(0, 3, 0);
point.castShadow = true;
point.shadow.mapSize.set(512, 512);     // point lights need 6 textures (cube)
scene.add(point);

// 6) Performance tuning
// - mapSize = balance: 512 / 1024 / 2048 / 4096
// - bias = thin objects need small bias; thick objects can use larger
// - normalBias = helps with very flat receivers (planes)
// - Disable shadows on meshes that are too small to matter
// - Reduce shadow camera frustum to ONLY cover what casts shadows
// - For huge scenes: cascaded shadow maps (CSM) via three-mesh-bvh or custom

// 7) Cascaded shadow maps — for very large scenes (terrains, open worlds)
// import { CSM } from 'three/examples/jsm/csm/CSM.js';
// const csm = new CSM({
//   maxFar: 200, cascades: 4, mode: 'practical',
//   parent: scene, shadowMapSize: 2048, lightDirection: new THREE.Vector3(-1, -1, -1),
//   camera, lightIntensity: 1.4,
// });
// In render loop: csm.update();

// 8) Common shadow artefacts + fixes
// - Acne (stripey pattern on lit surfaces) -> bias slightly more negative
// - Peter-panning (object floats off ground) -> bias less negative; normalBias up
// - Hard pixel edges -> larger mapSize OR softer ShadowMap type
// - Shadow cut off at frustum edge -> widen shadow camera

// 9) Mobile considerations
// - Shadows are expensive on mid-tier mobile
// - Drop to 512 or 1024 mapSize on mobile
// - Consider baked shadows (lightmap textures) for static scenes
// - Or disable runtime shadows entirely on detected low-end devices

// 10) Pitfalls
// - Forgetting receiveShadow on the ground -> light leaks through
// - Forgetting castShadow on meshes -> shadows missing
// - mapSize 8192 on mobile -> framerate dies
// - Shadow camera too big -> low effective resolution
// - Combining unrelated lights as shadow casters -> heavy GPU cost

Why it matters

Shadow quality lives in the shadow camera frustum size and the map size, not just the light intensity. Tighten the frustum to ONLY cover what actually casts shadows, raise the map size only where needed, and tune `bias` + `normalBias` until acne and peter-panning disappear. Cheaper, prettier, more reliable than cranking everything to 4096.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
renderer.shadowMap.enabled = true;
dir.castShadow = true;
mesh.castShadow = true;
floor.receiveShadow = true;
Try it Yourself »

Discussion

Loading…