Lighting & Shadows
Overview
Three.js implements a complete lighting and shadowing pipeline that combines dynamic light evaluation with shadow map occlusion testing. The system consists of two primary subsystems:
Lighting System (WebGLLights):
- Collects and organizes lights from the scene
- Transforms light properties into shader uniforms
- Supports directional, point, spot, hemisphere, and rect area lights
- Handles light probes for image-based lighting
Shadow System (WebGLShadowMap):
- Renders scene from each light's perspective into depth textures
- Supports PCF (Percentage Closer Filtering) and VSM (Variance Shadow Maps)
- Generates shadow coordinates in vertex shaders
- Samples shadow maps during fragment shading to determine occlusion
Rendering Pipeline:
WebGLLights.setup()processes lights and populates uniform arraysWebGLShadowMap.render()generates shadow maps (if enabled)WebGLPrograms.getParameters()determines required shader features- Fragment shaders evaluate lighting using
lights_fragment_beginandlights_physical_fragment - Shadow sampling modulates light contributions using
getShadow()functions
Key Components:
WebGLLights: Light data processing at src/renderers/webgl/WebGLLights.js157-581WebGLShadowMap: Shadow map generation at src/renderers/webgl/WebGLShadowMap.js35-597UniformsLib.lights: Light uniform definitions at src/renderers/shaders/UniformsLib.js119-197lights_physical_pars_fragment: PBR lighting equations at src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js
Lighting System Architecture
WebGLLights Overview
The WebGLLights class at src/renderers/webgl/WebGLLights.js157-581 manages light data transformation from scene objects to shader uniforms. It maintains internal state and provides two primary methods:
| Method | Purpose | Called From |
|---|---|---|
setup(lights) | Process lights, populate uniform arrays, update version hash | WebGLRenderer.render() before shader compilation |
setupView(lights, camera) | Transform light properties to camera view space | WebGLRenderer.render() before rendering |
State Structure [lines 163-204]:
state = {
version: 0, // Increments when light configuration changes
hash: { ... }, // Tracks light counts for change detection
ambient: [r, g, b], // Combined ambient light color
probe: [Vec3 × 9], // Spherical harmonics for light probes
// Per-light-type arrays
directional: [], // DirectionalLight uniforms
directionalShadow: [], // Shadow parameters
directionalShadowMap: [], // Shadow textures
directionalShadowMatrix: [], // World-to-shadow transforms
spot: [], // SpotLight uniforms
spotLightMap: [], // Light cookies/projectors
spotLightMatrix: [], // Light space transforms
spotShadow: [], // Shadow parameters
spotShadowMap: [], // Shadow textures
point: [], // PointLight uniforms
pointShadow: [], // Shadow parameters
pointShadowMap: [], // Cube shadow textures
pointShadowMatrix: [], // Shadow transforms
rectArea: [], // RectAreaLight uniforms
rectAreaLTC1: null, // LTC lookup table 1
rectAreaLTC2: null, // LTC lookup table 2
hemi: [], // HemisphereLight uniforms
numSpotLightShadowsWithMaps: 0,
numLightProbes: 0
}Light Uniform Caching
The UniformsCache and ShadowUniformsCache classes at src/renderers/webgl/WebGLLights.js8-145 create and cache uniform objects per light to avoid repeated allocation:
Diagram: Light Uniform Cache Architecture
Each light type has a specific uniform structure created once and reused. The cache is keyed by light.id [lines 16, 91].
Light Setup Process
The setup() method at src/renderers/webgl/WebGLLights.js212-489 processes the scene's lights and populates uniform arrays:
Diagram: Light Setup Flow
Light Sorting [line 233]: Lights are sorted by shadowCastingAndTexturingLightsFirst() [lines 151-155] to ensure shadow-casting lights are processed first, optimizing shader uniform array indexing.
View Space Transformation
The setupView() method at src/renderers/webgl/WebGLLights.js491-573 transforms light properties from world space to camera view space:
Diagram: View Space Transformation by Light Type
Why View Space? Transforming lights to view space simplifies shader calculations—the camera is at the origin looking down the -Z axis, making light direction and position calculations more efficient.
Light Uniform Structures
The UniformsLib.lights object at src/renderers/shaders/UniformsLib.js119-197 defines the shader uniform structure for all light types:
| Light Type | Uniform Structure | Properties |
|---|---|---|
| Ambient | ambientLightColor: [] | RGB color array |
| Light Probes | lightProbe: [] | 9 Vec3 coefficients for spherical harmonics |
| Directional | directionalLights: [] | {direction, color} per light |
| Directional Shadows | directionalLightShadows: [] | {shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize} |
directionalShadowMap: [] | Texture array | |
directionalShadowMatrix: [] | Matrix4 array | |
| Spot | spotLights: [] | {color, position, direction, distance, coneCos, penumbraCos, decay} |
| Spot Shadows | spotLightShadows: [] | Shadow parameters (same as directional) |
spotLightMap: [] | Cookie/projector textures | |
spotLightMatrix: [] | Light space transforms | |
spotShadowMap: [] | Shadow textures | |
| Point | pointLights: [] | {color, position, decay, distance} |
| Point Shadows | pointLightShadows: [] | {shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize, shadowCameraNear, shadowCameraFar} |
pointShadowMap: [] | Cube texture array | |
pointShadowMatrix: [] | Matrix4 array | |
| Hemisphere | hemisphereLights: [] | {direction, skyColor, groundColor} |
| Rect Area | rectAreaLights: [] | {color, position, width, height} |
ltc_1, ltc_2: null | Linearly Transformed Cosines lookup tables |
Shadow System
Shadow Map Types
Three.js supports multiple shadow mapping algorithms, each with different quality/performance tradeoffs:
| Shadow Type | Constant | Sampler Type | Filtering | Description |
|---|---|---|---|---|
| PCF Shadow Map | PCFShadowMap | sampler2DShadow | Linear (hardware PCF) | Percentage Closer Filtering using hardware comparison |
| Basic Shadow Map | (default fallback) | sampler2D | Nearest | Simple depth comparison, no filtering |
| VSM Shadow Map | VSMShadowMap | sampler2D (RG format) | Gaussian blur | Variance Shadow Maps with two-pass blur |
| PCF Soft (deprecated) | PCFSoftShadowMap | - | - | Deprecated, falls back to PCF |
The shadow map type is set on the renderer: renderer.shadowMap.type = PCFShadowMap.
Shadow Architecture
Component Diagram
Render Target Creation
Shadow maps are stored in render targets that differ based on light type and shadow map algorithm:
Standard Shadow Maps (Directional/Spot Lights)
Point Light Shadow Maps
Point lights require capturing depth in all directions, using a cube render target:
Shadow Rendering Pipeline
WebGLShadowMap.render() Execution Flow
Depth Material Generation
The getDepthMaterial() function at src/renderers/webgl/WebGLShadowMap.js418-505 selects or creates appropriate materials for shadow rendering. Materials are cached per (base material, original material) pair to avoid redundant cloning.
Shadow Side Mapping [line 51]:
The shadowSide object at src/renderers/webgl/WebGLShadowMap.js51 inverts face culling for shadow casting:
| Original Side | Shadow Side | Rationale |
|---|---|---|
FrontSide | BackSide | Front faces cast shadows from their back sides |
BackSide | FrontSide | Back faces cast shadows from their front sides |
DoubleSide | DoubleSide | Both sides cast shadows |
For VSM shadow maps, the material's shadowSide or side is used directly without inversion lines 471-479
Scene Traversal and Object Rendering
The renderObject() function at src/renderers/webgl/WebGLShadowMap.js507-568 recursively traverses the scene graph to render shadow-casting objects:
Filtering Criteria:
object.visible === trueobject.layers.test(camera.layers)passes- Object is
Mesh,Line, orPoints object.castShadow === trueOR (object.receiveShadow === trueANDtype === VSMShadowMap)- Not frustum culled OR
_frustum.intersectsObject(object)returns true
Rendering Steps:
- Update
object.modelViewMatrix = shadowCamera.matrixWorldInverse * object.matrixWorld[line 517] - Get geometry via
objects.update(object)[line 519] - For array materials, loop through
geometry.groupsand render each [lines 524-543] - Call
object.onBeforeShadow()[line 535, 549] - Call
renderer.renderBufferDirect()with depth material [line 537, 551] - Call
object.onAfterShadow()[line 539, 553] - Recursively process
object.children[lines 561-567]
Variance Shadow Maps (VSM)
Variance Shadow Maps store depth moments (mean and variance) in an RGFormat texture, which are blurred to achieve soft shadows with reduced light bleeding. The VSMPass() function at src/renderers/webgl/WebGLShadowMap.js375-416 performs a two-pass separable Gaussian blur:
Pass 1 (Vertical): Reads from shadow.map.depthTexture → Writes to shadow.mapPass Pass 2 (Horizontal): Reads from shadow.mapPass.texture → Writes to shadow.map
The full-screen triangle mesh used for the blur passes:
const fullScreenTri = new BufferGeometry();
fullScreenTri.setAttribute('position',
new BufferAttribute(
new Float32Array([-1, -1, 0.5, 3, -1, 0.5, -1, 3, 0.5]),
3
)
);
const fullScreenMesh = new Mesh(fullScreenTri, shadowMaterialVertical);This oversized triangle (vertices at (-1,-1), (3,-1), (-1,3)) covers the entire viewport when rendered.
Shader Materials:
shadowMaterialVertical: Vertical blur pass [line 53]shadowMaterialHorizontal: Horizontal blur pass,HORIZONTAL_PASSdefine [line 69]- Both use shader code from src/renderers/shaders/ShaderLib/vsm.glsl.js
- Configurable
VSM_SAMPLESdefine controls blur quality [lines 54-55, 379-386]
Full-Screen Triangle:
// Creates oversized triangle covering entire viewport [lines 71-78]
const fullScreenTri = new BufferGeometry();
fullScreenTri.setAttribute('position',
new BufferAttribute(
new Float32Array([-1, -1, 0.5, 3, -1, 0.5, -1, 3, 0.5]),
3
)
);Vertices at (-1,-1), (3,-1), (-1,3) ensure the triangle covers the full NDC space when rendered.
Shader Integration
Program Parameters for Lighting
The WebGLPrograms.getParameters() function at src/renderers/webgl/WebGLPrograms.js50-381 generates shader defines based on the scene's lighting configuration:
Light Count Parameters [lines 325-337]:
{
numDirLights: lights.directional.length,
numPointLights: lights.point.length,
numSpotLights: lights.spot.length,
numSpotLightMaps: lights.spotLightMap.length,
numRectAreaLights: lights.rectArea.length,
numHemiLights: lights.hemi.length,
numDirLightShadows: lights.directionalShadowMap.length,
numPointLightShadows: lights.pointShadowMap.length,
numSpotLightShadows: lights.spotShadowMap.length,
numSpotLightShadowsWithMaps: lights.numSpotLightShadowsWithMaps,
numLightProbes: lights.numLightProbes
}Shadow Parameters [lines 344-345]:
{
shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
shadowMapType: renderer.shadowMap.type
}These parameters are processed by replaceLightNums() at src/renderers/webgl/WebGLProgram.js214-231 to inject defines into shader code:
#define NUM_DIR_LIGHTS 2
#define NUM_POINT_LIGHTS 3
#define NUM_SPOT_LIGHTS 1
#define NUM_DIR_LIGHT_SHADOWS 2
#define USE_SHADOWMAP
#define SHADOWMAP_TYPE_PCFLighting Shader Chunks
Diagram: Lighting Shader Integration Flow
Key Shader Chunks:
| Chunk | Purpose | Location |
|---|---|---|
lights_pars_begin | Light uniform declarations and helper functions | src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js |
lights_physical_pars_fragment | PBR BRDF functions and material struct | src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js |
lights_fragment_begin | Initialize geometry vectors, iterate lights | src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js |
lights_fragment_end | Compute indirect lighting | src/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js |
shadowmap_pars_vertex | Shadow matrix uniforms and varyings | src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js |
shadowmap_vertex | Compute shadow coordinates | src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js |
shadowmap_pars_fragment | Shadow sampling functions | src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js |
Physically-Based Lighting
The lights_physical_pars_fragment shader chunk at src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js1-612 implements physically-based rendering equations for MeshStandardMaterial and MeshPhysicalMaterial.
PhysicalMaterial Structure [lines 5-58]:
struct PhysicalMaterial {
vec3 diffuseColor;
vec3 diffuseContribution;
vec3 specularColor;
vec3 specularColorBlended;
float roughness;
float metalness;
float specularF90;
float dispersion;
#ifdef USE_CLEARCOAT
float clearcoat;
float clearcoatRoughness;
vec3 clearcoatF0;
float clearcoatF90;
#endif
#ifdef USE_IRIDESCENCE
float iridescence;
float iridescenceIOR;
float iridescenceThickness;
vec3 iridescenceFresnel;
// ...
#endif
#ifdef USE_SHEEN
vec3 sheenColor;
float sheenRoughness;
#endif
#ifdef USE_TRANSMISSION
float transmission;
float transmissionAlpha;
float thickness;
float attenuationDistance;
vec3 attenuationColor;
#endif
#ifdef USE_ANISOTROPY
float anisotropy;
float alphaT;
vec3 anisotropyT;
vec3 anisotropyB;
#endif
};Core BRDF Functions:
| Function | Purpose | Lines |
|---|---|---|
V_GGX_SmithCorrelated() | GGX visibility term (geometry function) | 76-83 |
D_GGX() | GGX normal distribution function | 89-96 |
BRDF_GGX() | Main GGX specular BRDF | 155-200 |
BRDF_GGX_Clearcoat() | Clearcoat layer BRDF | 128-151 |
BRDF_Sheen() | Sheen (fabric) BRDF using Charlie distribution | 344-356 |
EnvironmentBRDF() | Image-based lighting using DFG LUT | 379-385 |
computeMultiscattering() | Energy-conserving multiscattering | 392-420 |
LTC_Evaluate() | Linearly Transformed Cosines for area lights | 254-315 |
Direct Lighting Function [lines 529-560]:
void RE_Direct_Physical(
const in IncidentLight directLight,
const in vec3 geometryPosition,
const in vec3 geometryNormal,
const in vec3 geometryViewDir,
const in vec3 geometryClearcoatNormal,
const in PhysicalMaterial material,
inout ReflectedLight reflectedLight
) {
float dotNL = saturate(dot(geometryNormal, directLight.direction));
vec3 irradiance = dotNL * directLight.color;
#ifdef USE_CLEARCOAT
// Clearcoat specular contribution
clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat(...);
#endif
#ifdef USE_SHEEN
sheenSpecularDirect += irradiance * BRDF_Sheen(...);
// Energy compensation for sheen
irradiance *= sheenEnergyComp;
#endif
// Specular with multiscattering
reflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter(...);
// Diffuse (Lambertian)
reflectedLight.directDiffuse += irradiance * BRDF_Lambert(material.diffuseContribution);
}Indirect Lighting Functions [lines 563-610]:
void RE_IndirectDiffuse_Physical(...) {
vec3 diffuse = irradiance * BRDF_Lambert(material.diffuseContribution);
#ifdef USE_SHEEN
// Sheen energy compensation
diffuse *= sheenEnergyComp;
#endif
reflectedLight.indirectDiffuse += diffuse;
}
void RE_IndirectSpecular_Physical(...) {
#ifdef USE_CLEARCOAT
clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF(...);
#endif
#ifdef USE_SHEEN
sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF(...);
#endif
// Compute multiscattering for dielectric and metallic contributions
computeMultiscatteringIridescence(..., singleScatteringDielectric, multiScatteringDielectric);
computeMultiscatteringIridescence(..., singleScatteringMetallic, multiScatteringMetallic);
// Mix based on metalness
vec3 singleScattering = mix(singleScatteringDielectric, singleScatteringMetallic, material.metalness);
vec3 multiScattering = mix(multiScatteringDielectric, multiScatteringMetallic, material.metalness);
reflectedLight.indirectSpecular += radiance * singleScattering;
reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;
}Light Iteration in Fragment Shaders
The lights_fragment_begin chunk at src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js1-205 iterates through all active lights and accumulates their contributions:
Diagram: Fragment Shader Light Loop
The loop uses #pragma unroll_loop_start / #pragma unroll_loop_end directives to unroll loops at compile time. The UNROLLED_LOOP_INDEX macro provides the loop counter for indexing into shadow arrays.
Depth Material Shaders
Two specialized materials handle depth rendering during shadow pass:
MeshDepthMaterial - Directional/Spot Lights
Used for directional and spot lights. Renders scene depth linearly mapped from camera near to far planes. Created once and reused at src/renderers/webgl/WebGLShadowMap.js44
Shader Definition at src/renderers/shaders/ShaderLib.js177-186:
depth: {
uniforms: mergeUniforms([
UniformsLib.common, // map, alphaMap, alphaTest
UniformsLib.displacementmap // displacementMap, scale, bias
]),
vertexShader: ShaderChunk.depth_vert,
fragmentShader: ShaderChunk.depth_frag
}The fragment shader outputs normalized depth: gl_FragDepth = (mvPosition.z + cameraNear) / (cameraFar - cameraNear)
MeshDistanceMaterial - Point Lights
Used for point lights. Calculates distance from fragment to light position, encoding it as depth for omnidirectional shadow maps. Created once and reused at src/renderers/webgl/WebGLShadowMap.js45
Shader Definition at src/renderers/shaders/ShaderLib.js270-284:
distance: {
uniforms: mergeUniforms([
UniformsLib.common,
UniformsLib.displacementmap,
{
referencePosition: { value: new Vector3() }, // Light world position
nearDistance: { value: 1 }, // Light near
farDistance: { value: 1000 } // Light far
}
]),
vertexShader: ShaderChunk.distance_vert,
fragmentShader: ShaderChunk.distance_frag
}The referencePosition uniform is set to the point light's world position in WebGLMaterials at src/renderers/webgl/WebGLMaterials.js498-500 The fragment shader calculates:
float dist = length(vWorldPosition - referencePosition);
gl_FragColor = packDepthToRGBA((dist - nearDistance) / (farDistance - nearDistance));Material Cache Management
To avoid redundant material cloning for objects requiring customized depth materials (e.g., with clipping, displacement, or alpha), the system maintains a two-level cache at src/renderers/webgl/WebGLShadowMap.js47:
_materialCache = {
[baseMaterialUuid]: { // _depthMaterial.uuid or _distanceMaterial.uuid
[objectMaterialUuid]: cachedCustomMaterial
}
}Cache Population [lines 440-462]: When getDepthMaterial() determines customization is needed, it:
- Looks up cache using
_materialCache[baseUuid][materialUuid] - On cache miss, clones the base material
- Stores clone in cache
- Adds
disposeevent listener to original material for cleanup
Cache Cleanup [lines 571-594]: The onMaterialDispose() function is called when any material is disposed:
function onMaterialDispose(event) {
const material = event.target;
material.removeEventListener('dispose', onMaterialDispose);
// Iterate through all cached materials
for (const id in _materialCache) {
const cache = _materialCache[id];
if (material.uuid in cache) {
const shadowMaterial = cache[material.uuid];
shadowMaterial.dispose(); // Dispose the shadow material
delete cache[material.uuid]; // Remove from cache
}
}
}This ensures that when a material with custom shadow materials is disposed, all associated shadow material clones are also properly disposed to prevent memory leaks.
Configuration and Usage
Shadow mapping is configured at multiple levels:
Renderer Level
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFShadowMap; // or VSMShadowMap
renderer.shadowMap.autoUpdate = true; // Auto-update each frame
renderer.shadowMap.needsUpdate = true; // Force single updateLight Level
Each light with shadows has a shadow property:
light.castShadow = true;
light.shadow.mapSize.set(2048, 2048);
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 500;
light.shadow.bias = 0.0001;
light.shadow.normalBias = 0.001;
light.shadow.radius = 1.0; // For VSM blurObject Level
Individual objects control shadow behavior:
object.castShadow = true; // Object casts shadows
object.receiveShadow = true; // Object receives shadows
// Custom depth materials for special rendering
object.customDepthMaterial = myDepthMaterial; // For directional/spot
object.customDistanceMaterial = myDistanceMaterial; // For point lights