Implementing Real-Time Shadows in Three.js

Shadow rendering in Three.js relies on a specific pipeline involving the renderer, light source, geometry, and material configurations. By default, the engine skips shadow calculations to presreve performance. To activate this feature, four distinct components must be properly configured.

First, enable the shadow mapping system on the renderer instance. Second, select a light source capable of projecting shadows, such as SpotLight or DirectionalLight, and assign it the projection flag. Third, prepare a target surface that will capture the illumination occlusion by toggling its receive property. Fourth, designate the geometry that blocks light to actively cast shadows.

The following implementation demonstrates a minimal, production-ready setup. It constructs a scene with a receiving floor, a casting block, and an active spotlight, then configures the necessary shadow flags.

import * as THREE from 'three';

// Core setup
const world = new THREE.Scene();
const view = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500);
view.position.set(-25, 40, 40);
view.lookAt(new THREE.Vector3());

const gl = new THREE.WebGLRenderer({ antialias: true });
gl.setSize(window.innerWidth, window.innerHeight);
gl.setClearColor(new THREE.Color('#eeeeee'));
document.body.appendChild(gl.domElement);

// Activate shadow rendering pipeline
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFSoftShadowMap;

// Light source configuration
const beam = new THREE.SpotLight(0xffffff, 1.5);
beam.position.set(-30, 50, 20);
beam.target.position.set(0, 0, 0);
world.add(beam.target);
beam.castShadow = true;

// Optimize shadow resolution and frustum
beam.shadow.mapSize.width = 2048;
beam.shadow.mapSize.height = 2048;
beam.shadow.camera.near = 1;
beam.shadow.camera.far = 150;
beam.shadow.bias = -0.0005;

// Receiving surface
const floorGeo = new THREE.PlaneGeometry(80, 80);
const floorMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.8 });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
world.add(floor);

// Casting object
const blockGeo = new THREE.BoxGeometry(8, 8, 8);
const blockMat = new THREE.MeshStandardMaterial({ color: 0xff4444 });
const block = new THREE.Mesh(blockGeo, blockMat);
block.position.set(5, 4, -10);
block.castShadow = true;
world.add(block);

// Render loop
function animate() {
  requestAnimationFrame(animate);
  gl.render(world, view);
}
animate();

Enabling shadows requires explicit permission across multiple objects. The renderer initializes the depth buffer calculation. The light source projects this depth data through its viewing frustum. Any mesh intersecting this volume checks its castShadow boolean to participate as an occluder. Similarly, surfaces checking their receiveShadow attribute will sample the projected depth texture during rasterization.

Performance costs scale with shadow resoluiton and the number of active lights. Adjusting mapSize dictates pixel density on the shadow texture. Narrowing the camera.near and camera.far values ensures the depth buffer captures only the relevant viewing range, reducing artifacts and memory usage. For smoother edges, switching between hardware filtering types like PCFSoftShadowMap or PCFShadowMap balances rendering overhead against visual sharpness.

Tags: Three.js WebGL 3D Rendering Shadows javascript

Posted on Fri, 15 May 2026 01:21:16 +0000 by tony-kidsdirect