DEV Community

Cover image for Mastering Shadows in Three.js: Setup, Configuration, and Optimization
Peter Riding
Peter Riding

Posted on

Mastering Shadows in Three.js: Setup, Configuration, and Optimization

Shadows are essential for adding depth and realism to Three.js scenes. They help ground objects and enhance visual interest, but configuring them effectively usually involves a bit of trial and error the first time around. I’ve lost count of how often a scene looked “broken” simply because one mesh wasn’t set to receive shadows. This guide builds on fundamental lighting concepts, focusing on practical implementation for various light types. We'll cover enabling shadows, tuning for quality, and integrating them cleanly with the rest of your scene.

Enabling Shadows: The Basics

To incorporate shadows, start by activating shadow mapping in the renderer. This prepares the system to calculate and render shadows for compatible lights and objects.

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
Enter fullscreen mode Exit fullscreen mode

The type property determines shadow appearance and performance:

  • BasicShadowMap: Fastest option, but results in hard, blocky edges. Suitable for quick tests.
  • PCFShadowMap: Provides softer edges with moderate performance.
  • PCFSoftShadowMap: Offers smoother, more realistic shadows; a balanced choice for most applications.
  • VSMShadowMap: Produces very soft shadows, but can introduce light bleeding or blur without careful tuning.

Additionally, configure individual elements:

  • For lights: light.castShadow = true;
  • For meshes that cast shadows: mesh.castShadow = true;
  • For meshes that receive shadows: mesh.receiveShadow = true;

If nothing shows up, double-check these three flags first — it’s almost always the culprit in fresh setups, especially for floors and large background meshes.

Configuring Shadows for Directional Lights

Directional lights, often used for simulating sunlight or moonlight, require specific shadow camera adjustments. The shadow map resolution and frustum define quality and coverage.

  • mapSize: Controls texture resolution. Lower values (e.g., 256) improve performance at the cost of sharpness; higher values yield crisper shadows but increase GPU load.
  • Frustum bounds (top, right, bottom, left): Limit the shadow-casting area. Tighter bounds dramatically improve detail in the focused region.
  • near and far: Set the depth range. Objects outside this won't cast shadows; a narrower range also reduces common precision artifacts.

Example configuration for a contained scene:

directionalLight.shadow.camera.top = 8;
directionalLight.shadow.camera.right = 8;
directionalLight.shadow.camera.bottom = -8;
directionalLight.shadow.camera.left = -8;
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 20;
Enter fullscreen mode Exit fullscreen mode

In practice, settings like a 256 mapSize and ±8 bounds work well for balanced results, and tightening the frustum usually gives a bigger quality boost than simply cranking the resolution.

Setting Purpose Example Trade-off
mapSize Shadow resolution 256 (low) or 1024 (high) Performance vs. sharpness
top/right/bottom/left Frustum bounds ±8 Coverage vs. quality
near/far Depth range 1 / 20 Precision vs. artifacts

When tuning directional lights, helpers are invaluable:

scene.add(new THREE.CameraHelper(directionalLight.shadow.camera));
Enter fullscreen mode Exit fullscreen mode

Seeing the shadow frustum in the scene often explains blurry edges or missing shadows instantly.

Shadows Across Light Types

Shadow implementation varies by light type, each using a different camera projection. Common properties like shadow.mapSize and shadow.camera.far apply universally, but additional settings differ.

  • PointLight: Employs a cubemap for omnidirectional shadows. Adjust shadow.mapSize per face and shadow.camera.far for range. These are resource-intensive, so I usually reserve them for small, focused light sources.
  • DirectionalLight: Uses an orthographic camera for parallel projection. Configure frustum bounds and clipping planes carefully, as described above.
  • SpotLight: Relies on a perspective camera. Set shadow.mapSize, shadow.camera.fov, near, and far to match the light's cone.

Reference table for quick setup:

Light Type Shadow Camera Key Settings
PointLight Cubemap mapSize (per face), camera.far
DirectionalLight Orthographic mapSize, top/right/bottom/left, near/far
SpotLight Perspective mapSize, fov/near/far

I recommend testing shadow setups in a stripped-down scene with a cube and a plane before layering in complex geometry — it saves a lot of guessing later.

Integration and Fine-Tuning

To avoid overly dark shadowed areas, combine shadows with a low-intensity AmbientLight for subtle fill:

const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
Enter fullscreen mode Exit fullscreen mode

For iterative adjustments, a debugging tool like dat.GUI is extremely helpful:

gui.add(light.shadow, 'mapSize.width')
  .min(128).max(2048).step(128)
  .name('Shadow Resolution');

gui.add(light.shadow.camera, 'near')
  .min(0.1).max(10).step(0.1);
Enter fullscreen mode Exit fullscreen mode

When shadows look glitchy, speckled, or slightly detached from objects, small tweaks to:

  • light.shadow.bias
  • light.shadow.normalBias

often fix acne or floating edges without needing to push the shadow resolution higher.

Shadows also interact heavily with materials; incorporating ARM textures (ambient occlusion, roughness, metalness) can dramatically improve how light and shadow behave across surfaces.

Conclusion

Proper shadow setup transforms flat Three.js scenes into immersive environments. Start by enabling shadows everywhere they’re needed, tighten each light’s shadow camera before increasing resolution, and only push map sizes once the basics are dialed in. In my experience, those three steps alone account for most real-world shadow improvements.

Experiment with these techniques in your own projects, and don’t be afraid to temporarily drop quality while tuning — it’s much easier to debug shadows when everything runs fast.

Top comments (0)