Three.js Lighting: A Practical Guide

Three.js has six light types (AmbientLight, DirectionalLight, PointLight, SpotLight, HemisphereLight and RectAreaLight), and most scenes only need two or three of them together: one DirectionalLight or HemisphereLight for the main light source, an AmbientLight or a second dim light to fill shadows, and PointLight or SpotLight for local accents.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The six light types, and when to use each

  • AmbientLight: uniform light from every direction, no shadows, no direction. Cheap and flat; use it as a fill light so shadowed areas aren't pure black, not as a primary source.
  • DirectionalLight: parallel rays, like the sun. The standard choice for outdoor scenes and the most common "key light"; it's the one that casts real shadows via castShadow.
  • HemisphereLight: a gradient between a sky color and a ground color, no shadows. A cheap, very effective substitute for full ambient occlusion in outdoor scenes; it reads as "sky bounce light" for almost no cost.
  • PointLight: radiates in all directions from a point, like a bare bulb. Falls off with distance (decay). Good for lamps, torches, small local sources.
  • SpotLight: a cone of light from a point, with angle and penumbra for the cone's softness. Flashlights, stage lighting, car headlights.
  • RectAreaLight: a rectangular area light, like a softbox or a window. Physically the most correct-looking for product shots, but only works with MeshStandardMaterial/MeshPhysicalMaterial and does not cast shadows.

A believable default setup

Borrow the photography "three-point lighting" idea:

  1. Key light: a DirectionalLight at moderate intensity, angled from above and to one side. This is the light that casts shadows.
  2. Fill light: a dim AmbientLight or a second DirectionalLight from the opposite side at low intensity, so shadow areas aren't pure black without washing out the key light's contrast.
  3. Rim/accent: optional, a SpotLight or PointLight behind or beside the subject to separate it from the background.

For outdoor or "natural" scenes, swap the ambient fill for a HemisphereLight. It's cheap and reads far more convincingly as sky bounce than flat ambient does.

Getting shadows to actually work

Shadows in Three.js require three separate switches, and forgetting any one of them is the most common "why don't I see shadows" bug:

  1. renderer.shadowMap.enabled = true on the renderer.
  2. light.castShadow = true on the light (only DirectionalLight, PointLight and SpotLight support this).
  3. mesh.castShadow = true on objects that should cast, and mesh.receiveShadow = true on objects (like a ground plane) that should receive them.

For a DirectionalLight, also tune light.shadow.camera.{left,right,top,bottom,near,far}. The default shadow camera frustum is small and shadows outside it simply don't render, which looks like shadows "not working" when they're actually just clipped.

Tools and libraries

Code

Three-point rig (r181)
1import * as THREE from 'three'
2
3renderer.shadowMap.enabled = true
4
5const key = new THREE.DirectionalLight(0xffffff, 3)
6key.position.set(5, 8, 5)
7key.castShadow = true
8key.shadow.camera.left = -10
9key.shadow.camera.right = 10
10key.shadow.camera.top = 10
11key.shadow.camera.bottom = -10
12scene.add(key)
13
14const fill = new THREE.HemisphereLight(0xbfd4ff, 0x2b2b2b, 0.6)
15scene.add(fill)
16
17const rim = new THREE.SpotLight(0xffffff, 5, 20, Math.PI / 6, 0.4)
18rim.position.set(-4, 5, -4)
19scene.add(rim)
20
21mesh.castShadow = true
22ground.receiveShadow = true
Material response to lights (TSL, r181)
1import { MeshStandardNodeMaterial } from 'three/webgpu'
2import { color, roughness, metalness } from 'three/tsl'
3
4// TSL materials respond to scene lights automatically, same as
5// MeshStandardMaterial, no lighting-specific TSL code required for the
6// standard PBR response; you only reach for node-level control when
7// authoring custom lighting math (e.g. a toon shading ramp).
8const material = new THREE.MeshStandardNodeMaterial()
9material.colorNode = color(0xe5e5e5)
10material.roughnessNode = roughness(0.6)
11material.metalnessNode = metalness(0.1)

Learn this properly

Learn Practical TSL

Your First Node Material

Understand how materials sample color and roughness before tuning how they respond to light.

Start the lesson (6 minutes)

Frequently asked questions

Why aren't my Three.js shadows showing up?

Check all three switches: renderer.shadowMap.enabled = true, light.castShadow = true on the light (only Directional/Point/SpotLight support it), and castShadow/receiveShadow set on the relevant meshes. For DirectionalLight, also check the shadow camera frustum isn't clipping the shadow area.

What's the difference between AmbientLight and HemisphereLight?

AmbientLight adds a flat, uniform amount of light from every direction with no gradient. HemisphereLight blends between a sky color (from above) and a ground color (from below), which reads far more like real outdoor bounce light for a similar cost.

How many lights should a Three.js scene have?

Most scenes need 2-4: one key light (usually DirectionalLight) that casts shadows, one fill light (AmbientLight or HemisphereLight) so shadows aren't pure black, and optionally an accent PointLight or SpotLight. More than that usually adds cost without adding clarity.

Keep reading