Skip to content

Three.js - Lights & 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 with new 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 1 is a good starting point.

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.

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 code
renderer.shadowMap.enabled = true;
// For softer, more realistic shadows (at a performance cost)
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Other types: THREE.BasicShadowMap, THREE.PCFShadowMap, THREE.VSMShadowMap

Note

Choosing a shadowMap.type is a trade-off between performance and quality. PCFSoftShadowMap is a popular choice for visually pleasing soft edges.

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 light
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
// Enable shadow casting for this light
directionalLight.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 frustum
const 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.

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 shadows
anotherObject.castShadow = true;
anotherObject.receiveShadow = true;

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 acne
directionalLight.shadow.normalBias = 0.02; // Fixes peter-panning

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.

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 illumination
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// Directional light for casting shadows
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(5, 6, 2);
directionalLight.castShadow = true; // Enable shadow casting
scene.add(directionalLight);
// Configure shadow properties
directionalLight.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;
// Helpers
const 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
*/
// Material
const material = new THREE.MeshStandardMaterial({ color: 0x0080ff });
material.roughness = 0.6;
material.metalness = 0.2;
// Floor
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(20, 20),
material
);
floor.rotation.x = -Math.PI * 0.5;
floor.receiveShadow = true; // Floor receives shadows
scene.add(floor);
// Sphere
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
material
);
sphere.position.y = 1;
sphere.castShadow = true; // Sphere casts shadows
scene.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 renderer
renderer.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));
});
Light TypeDescription & Use Case
1. AmbientLightIlluminates 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. DirectionalLightEmits 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. PointLightEmits 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. SpotLightEmits 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. HemisphereLightA 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. RectAreaLightEmits 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.