Shadows bring your scene to life
Shadows anchor objects in space. Without them, a sphere hovering above a plane looks like it's pasted on top of a flat image. Add a shadow beneath it, and suddenly the viewer understands the relationship between it and its surroundings.
Three.js doesn't compute shadows automatically. You have to explicitly opt in at three levels: the renderer, the light, and each mesh. This design is intentional because shadows are one of the most expensive operations in real-time 3D, so Three.js gives you full control over where you spend that budget. You have to be careful with how complex your shadow design is. Go overboard and it will tank your framerate.
The three-step setup for shadows
Before any shadow appears in your scene, you need to enable three things:
- The renderer must have shadow mapping turned on.
- The light must be marked as a shadow caster.
- Each mesh must declare whether it casts shadows, receives shadows, or both.
Miss any one of these and you'll see no shadows at all. God knows this is the most common source of "why aren't my shadows working?" for me. It's like the "phone, wallet, keys" of leaving the house. "Renderer, light, mesh", say it with me.
The demo above shows the minimum viable shadow setup which includes a directional light casting shadows from a sphere onto a floor plane. The sphere has castShadow = true, the floor has receiveShadow = true, and the light and renderer are both shadow-enabled.
// Step 1: Enable shadow maps on the renderer
renderer.shadowMap.enabled = true;
// Step 2: Make the light cast shadows
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.castShadow = true;
scene.add(dirLight);
// Step 3: Tell meshes to cast and/or receive shadows
sphere.castShadow = true; // This object blocks light
floor.receiveShadow = true; // This surface shows shadows
// Common gotcha: if you forget ANY of these three steps,
// no shadows will appear. All three are required.Shadow map yypes
Three.js offers several shadow mapping algorithms, each with different quality and performance characteristics. You set the algorithm on the renderer, and it applies to all shadows in the scene.
BasicShadowMap
THREE.BasicShadowMap is the fastest and simplest algorithm. It produces hard-edged shadows with visible aliasing (staircase artifacts along shadow edges). Each texel in the shadow map maps directly to a pixel, so no filtering and no smoothing. It looks so-so, but it is cheap and relatively fast.
Notice the jagged, pixelated edges of the shadow? This is the raw shadow map with no filtering applied. It's fast but rarely looks good enough for final output.
renderer.shadowMap.type = THREE.BasicShadowMap;
// Pros: Fastest shadow rendering
// Cons: Hard, aliased edges — visible stairstepping
// Use when: Performance is critical and shadow quality
// doesn't matter (e.g., mobile, VR, many shadow casters)PCFShadowMap (Default)
THREE.PCFShadowMap is the default algorithm. PCF stands for Percentage-Closer Filtering. It samples multiple points around each shadow texel and averages them, producing softer edges. The shadow is still fundamentally hard (the penumbra doesn't grow with distance), but the aliasing is smoothed out.
The shadow edges are noticeably smoother than BasicShadowMap. This is the default for a reason. It's a good balance of quality and performance for most scenes. Unless you have a damned good reason, you should be using this.
renderer.shadowMap.type = THREE.PCFShadowMap;
// Pros: Smooth edges, reasonable performance
// Cons: Shadows are still uniformly sharp (no soft penumbra)
// Use when: Default choice for most projectsPCFSoftShadowMap
THREE.PCFSoftShadowMap extends PCF with a larger filter kernel, producing visibly softer shadows. The edges blur more, which hides aliasing better and gives a more natural appearance. The cost is higher than standard PCF but still reasonable for most desktop scenes.
Compare this to the PCF demo above and you'll see that the shadow edges are noticeably softer and more diffuse. This is often the best choice when you want natural-looking shadows without the cost of VSM.
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Pros: Soft, natural-looking edges
// Cons: Slightly more expensive than PCF
// Use when: You want softer shadows without
// the artifacts that come with VSMVSMShadowMap
THREE.VSMShadowMap uses Variance Shadow Mapping, a fundamentally different approach to generating shadows. Instead of storing a single depth value per texel, it stores the depth and the depth squared, which lets the shader compute a probabilistic soft shadow. VSM produces the softest shadows and can be blurred further, but it's prone to light bleeding, which you'll see in your scene as bright halos where two shadow casters overlap.
These shadows are the softest of all four algorithms. VSM works well for single shadow casters, but watch out for light bleeding artifacts in complex scenes with overlapping shadows.
renderer.shadowMap.type = THREE.VSMShadowMap;
// Pros: Softest shadows, can be blurred with radius
// Cons: Light bleeding artifacts, more GPU memory
// Use when: Single or few shadow casters, soft aesthetic
// VSM supports shadow radius for additional blur:
dirLight.shadow.radius = 4; // Only works with VSMShadow camera configuration
Every shadow-casting light has a hidden camera, the shadow camera, that renders the scene from the light's perspective into a depth texture (the shadow map). The quality and coverage of your shadows depends entirely on how this camera is configured.
For a DirectionalLight, the shadow camera is orthographic. Its left, right, top, and bottom bounds define the rectangular area that receives shadows. Anything outside this box won't cast or receive shadows.
The demo above shows a camera helper visualizing the shadow camera's frustum. The blue box outlines exactly which area of the scene is covered by the shadow map. Objects outside this box won't have shadows. Making the box too large wastes resolution; making it too small clips shadows.
// Configure the directional light's shadow camera
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.castShadow = true;
// Shadow camera bounds (orthographic for DirectionalLight)
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
// Shadow map resolution (higher = sharper, more expensive)
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
// Visualize the shadow camera during development
const helper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(helper);
// Common resolutions:
// 512x512 — low quality, fast
// 1024x1024 — medium (good default)
// 2048x2048 — high quality
// 4096x4096 — very high (expensive)Shadow bias and artifacts
Shadow mapping suffers from a fundamental precision problem: the shadow map is a finite-resolution grid, and when a surface tries to shadow itself, tiny rounding errors create a pattern of alternating light and dark bands called shadow acne. The bias property offsets the shadow depth comparison to eliminate this artifact.
But push the bias too far and you get peter panning — the shadow detaches from the base of the object and appears to float. Finding the right bias is a balancing act between these two artifacts.
The demo above shows three objects. The left object has no bias applied. Notice the moire pattern of shadow acne on the surfaces. The center object has a moderate bias that eliminates acne. The right object has excessive bias, i.e. the shadow has detached from the base (peter panning).
// Shadow bias: fixes shadow acne vs peter panning
dirLight.shadow.bias = -0.005; // Small negative value
dirLight.shadow.normalBias = 0.02; // Pushes along surface normal
// bias: offsets the depth comparison directly.
// Too little → shadow acne (self-shadowing artifacts)
// Too much → peter panning (shadow floats away)
// Start at -0.005 and adjust from there.
// normalBias: offsets along the surface normal.
// Helps with acne on curved surfaces without
// causing peter panning. Usually 0.01 to 0.05.
// Typical good starting values:
dirLight.shadow.bias = -0.005;
dirLight.shadow.normalBias = 0.02;Shadows per light type
Not all lights support shadows, and those that do handle them differently. Here's the breakdown:
- DirectionalLight: Orthographic shadow camera. Single shadow map pass. Best for outdoor/sun shadows. Cheapest shadow-capable light.
- SpotLight: Perspective shadow camera matching the cone. Single shadow map pass. Good balance of cost and quality for focused lights.
- PointLight: Renders a cube shadow map (six passes, one per face). Most expensive shadow type. Use sparingly.
- AmbientLight: No shadows. By definition, ambient light has no direction.
- HemisphereLight: No shadows. Sky/ground fill has no single direction to cast from.
- RectAreaLight: No native shadow support in Three.js.
SpotLight shadows
The spotlight's shadow camera is a perspective camera that matches the cone's angle and range. This means the shadow map resolution is concentrated where the light actually shines, making spotlights more shadow-efficient than directional lights for focused areas.
const spotLight = new THREE.SpotLight(0xffffff, 80, 30);
spotLight.castShadow = true;
spotLight.position.set(0, 8, 0);
// The shadow camera automatically matches the spotlight cone
// but you can still configure resolution and bias:
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.bias = -0.005;
// The shadow camera's fov matches the spotlight angle,
// and the far plane matches the spotlight distance.
// You rarely need to adjust these manually.PointLight shadows
Point lights emit in all directions, so a single 2D shadow map can't capture the full scene. Instead, Three.js renders six shadow maps, one for each face of a cube, creating a cube shadow map. This means a single point light shadow costs six render passes, making it the most expensive shadow type by far.
const pointLight = new THREE.PointLight(0xff9944, 50, 20, 2);
pointLight.castShadow = true;
pointLight.position.set(0, 4, 0);
// Resolution applies to EACH of the 6 cube faces
// So 1024x1024 actually uses 6x the memory of a
// directional light at the same resolution
pointLight.shadow.mapSize.width = 512;
pointLight.shadow.mapSize.height = 512;
pointLight.shadow.bias = -0.005;
// Point light shadow cameras use near/far:
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 20;Performance guidelines
Shadows are where performance budgets go to die in Three.js. Every shadow-casting light adds at least one full scene render pass (six for point lights). Here are the rules of thumb:
- Minimize shadow casters. Only enable
castShadowon objects that actually need visible shadows. A background mountain range doesn't need to cast shadows. - Minimize shadow receivers. Only enable
receiveShadowon surfaces where shadows will actually land. The sky doesn't need to receive shadows. - Use the smallest shadow map that looks acceptable. 1024x1024 is often sufficient. 2048x2048 is a luxury. 4096x4096 is rarely worth the cost.
- Tighten the shadow camera frustum. The shadow camera should cover only the area that matters. A directional light shadow camera covering a 100x100 area will produce blurry shadows; the same resolution covering 10x10 will be sharp.
- Prefer directional and spot shadows over point shadows. One pass versus six is a significant difference.
- Consider baked shadows. For static scenes, you can pre-compute shadows as textures and apply them to the floor. Zero runtime cost.
// Performance-optimized shadow setup
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
sunLight.castShadow = true;
// Tight frustum — only cover the play area
sunLight.shadow.camera.left = -8;
sunLight.shadow.camera.right = 8;
sunLight.shadow.camera.top = 8;
sunLight.shadow.camera.bottom = -8;
sunLight.shadow.camera.near = 1;
sunLight.shadow.camera.far = 20;
// Moderate resolution
sunLight.shadow.mapSize.set(1024, 1024);
// Only flag objects that need shadows
player.castShadow = true;
player.receiveShadow = true;
ground.receiveShadow = true;
// decorativeParticles.castShadow = false; // skip these