Three.js - Lights & Shadows
Modern Three.js - Lights and Shadows
Section titled “Modern Three.js - Lights and Shadows”In 3D graphics, lights are essential for making objects visible and creating a realistic or stylized look. Three.js provides several types of lights that simulate real-world lighting conditions. However, not all materials react to light. Basic materials like MeshBasicMaterial are self-illuminating. The materials you’ll use most often, which are part of the Physically-Based Rendering (PBR) workflow, such as MeshStandardMaterial and MeshPhysicalMaterial, are designed to react to light in a physically plausible way. Legacy materials like MeshLambertMaterial and MeshPhongMaterial also react to light but are generally not recommended for new projects.
All lights in Three.js share two fundamental properties:
- color − (optional) The hexadecimal color of the light. Defaults to
0xffffff(white). Can be set withnew THREE.Color(0xffffff). - intensity − (optional) A numeric value for the light’s strength or brightness. The effective range depends on the renderer’s configuration, but a default of
1is a good starting point.
Casting Shadows: A 3-Step Process
Section titled “Casting Shadows: A 3-Step Process”Shadows add depth and realism to a scene but are computationally expensive. Therefore, you must explicitly enable them. The process involves configuring the renderer, the lights, and the objects in the scene.
Step 1: Configure the Renderer
Section titled “Step 1: Configure the Renderer”First, you must enable shadow mapping in the WebGLRenderer. Shadow Mapping is a technique where the scene is rendered from the light’s perspective to create a ‘shadow map’.
// In your initialization coderenderer.shadowMap.enabled = true;
// For softer, more realistic shadows (at a performance cost)renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Other types: THREE.BasicShadowMap, THREE.PCFShadowMap, THREE.VSMShadowMapNote
Choosing a shadowMap.type is a trade-off between performance and quality. PCFSoftShadowMap is a popular choice for visually pleasing soft edges.
Step 2: Configure the Light
Section titled “Step 2: Configure the Light”Not all lights can cast shadows. DirectionalLight, PointLight, and SpotLight are the primary shadow-casting lights. You need to enable shadow casting on each light you want to generate shadows.
// For a directional lightconst directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
// Enable shadow casting for this lightdirectionalLight.castShadow = true;
// --- Shadow Quality ---// Set the resolution of the shadow map. Higher values mean better quality but lower performance.// Values must be powers of 2 (e.g., 512, 1024, 2048).directionalLight.shadow.mapSize.width = 1024;directionalLight.shadow.mapSize.height = 1024;
// --- Shadow Camera Frustum ---// Define the area in which shadows will be rendered. This is crucial for performance and quality.// A smaller, tighter frustum results in higher quality shadows.directionalLight.shadow.camera.near = 0.5;directionalLight.shadow.camera.far = 50;directionalLight.shadow.camera.left = -10;directionalLight.shadow.camera.right = 10;directionalLight.shadow.camera.top = 10;directionalLight.shadow.camera.bottom = -10;
// For debugging the shadow camera frustumconst shadowCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);scene.add(shadowCameraHelper);The shadow.camera properties define a virtual box. Only objects within this box will cast shadows. Adjusting this box to be as tight as possible around your scene’s shadow-casting objects is a key optimization.
Step 3: Configure Scene Objects
Section titled “Step 3: Configure Scene Objects”Finally, you must specify which objects in your scene will cast shadows and which will receive them. An object can do both.
// The object that casts the shadow (e.g., a cube)cube.castShadow = true;
// The object that receives the shadow (e.g., a plane/floor)plane.receiveShadow = true;
// An object can both cast and receive shadowsanotherObject.castShadow = true;anotherObject.receiveShadow = true;Common Shadow Artifacts and Fixes
Section titled “Common Shadow Artifacts and Fixes”You might encounter visual glitches with shadows. Here are two common ones:
- Shadow Acne: Strange line patterns on surfaces that receive shadows. This happens because of floating-point precision errors. Fix it by applying a small ‘bias’.
- Peter Panning: A gap appears between the shadow and the object casting it, making the object look like it’s floating. Fix it with ‘normal bias’.
// Adjust these values carefully. Start small.directionalLight.shadow.bias = -0.0001; // Fixes shadow acnedirectionalLight.shadow.normalBias = 0.02; // Fixes peter-panningExample: Modern Shadow Setup
Section titled “Example: Modern Shadow Setup”This example demonstrates a modern setup using lil-gui for controls and best practices for shadows. This code assumes a project setup using a bundler like Vite.
main.js
Section titled “main.js”import * as THREE from 'three';import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';import GUI from 'lil-gui';
/** * Base Setup */const gui = new GUI();const scene = new THREE.Scene();scene.background = new THREE.Color(0x1a2b34);
/** * Sizes */const sizes = { width: window.innerWidth, height: window.innerHeight};
/** * Camera */const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100);camera.position.set(4, 5, 6);scene.add(camera);
/** * Lights */// Ambient light for basic illuminationconst ambientLight = new THREE.AmbientLight(0xffffff, 0.5);scene.add(ambientLight);
// Directional light for casting shadowsconst directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);directionalLight.position.set(5, 6, 2);directionalLight.castShadow = true; // Enable shadow castingscene.add(directionalLight);
// Configure shadow propertiesdirectionalLight.shadow.mapSize.width = 1024;directionalLight.shadow.mapSize.height = 1024;directionalLight.shadow.camera.near = 0.5;directionalLight.shadow.camera.far = 20;directionalLight.shadow.bias = -0.001;
// Helpersconst directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 1);scene.add(directionalLightHelper);
const shadowCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);scene.add(shadowCameraHelper);shadowCameraHelper.visible = false; // Initially hidden
/** * Objects */// Materialconst material = new THREE.MeshStandardMaterial({ color: 0x0080ff });material.roughness = 0.6;material.metalness = 0.2;
// Floorconst floor = new THREE.Mesh( new THREE.PlaneGeometry(20, 20), material);floor.rotation.x = -Math.PI * 0.5;floor.receiveShadow = true; // Floor receives shadowsscene.add(floor);
// Sphereconst sphere = new THREE.Mesh( new THREE.SphereGeometry(1, 32, 32), material);sphere.position.y = 1;sphere.castShadow = true; // Sphere casts shadowsscene.add(sphere);
/** * Renderer */const canvas = document.querySelector('canvas.webgl');const renderer = new THREE.WebGLRenderer({ canvas });renderer.setSize(sizes.width, sizes.height);renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Enable shadows in the rendererrenderer.shadowMap.enabled = true;renderer.shadowMap.type = THREE.PCFSoftShadowMap;
/** * Controls */const controls = new OrbitControls(camera, canvas);controls.enableDamping = true;
/** * GUI Controls */gui.add(ambientLight, 'intensity').min(0).max(3).step(0.01).name('Ambient Intensity');gui.add(directionalLight, 'intensity').min(0).max(5).step(0.01).name('Sun Intensity');gui.add(directionalLight.position, 'x').min(-10).max(10).step(0.1).name('Sun X');gui.add(directionalLight.position, 'y').min(-10).max(10).step(0.1).name('Sun Y');gui.add(directionalLight.position, 'z').min(-10).max(10).step(0.1).name('Sun Z');gui.add(shadowCameraHelper, 'visible').name('Debug Shadow Cam');
/** * Animation Loop */const clock = new THREE.Clock();
const tick = () => { const elapsedTime = clock.getElapsedTime();
// Animate sphere sphere.position.x = Math.sin(elapsedTime) * 3; sphere.position.z = Math.cos(elapsedTime) * 3; sphere.rotation.y += 0.01;
controls.update(); renderer.render(scene, camera); window.requestAnimationFrame(tick);};
tick();
/** * Handle Resize */window.addEventListener('resize', () => { sizes.width = window.innerWidth; sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height; camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));});Overview of Three.js Lights
Section titled “Overview of Three.js Lights”| Light Type | Description & Use Case |
|---|---|
| 1. AmbientLight | Illuminates all objects in the scene equally from all directions. Use Case: To provide base lighting and prevent shadows from being completely black. Cannot cast shadows. |
| 2. DirectionalLight | Emits light in a specific direction, with parallel rays. Use Case: Simulating a very distant light source like the sun. Excellent for casting crisp, parallel shadows. |
| 3. PointLight | Emits light in all directions from a single point in space. Use Case: Simulating a bare light bulb. Shadows radiate out from the light’s position. |
| 4. SpotLight | Emits light from a point in a cone shape. Use Case: Simulating a flashlight, spotlight, or car headlight. Gives you precise control over the shadow area. |
| 5. HemisphereLight | A light source positioned directly above the scene, with color fading from the sky color to the ground color. Use Case: Simulating natural, ambient outdoor light. Does not cast shadows. |
| 6. RectAreaLight | Emits light uniformly across the face of a rectangular plane. Use Case: Simulating bright windows or studio lighting. Only works with MeshStandardMaterial and MeshPhysicalMaterial. Shadow support is complex and limited. |