Skip to content

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:

  1. WebGLLights.setup() processes lights and populates uniform arrays
  2. WebGLShadowMap.render() generates shadow maps (if enabled)
  3. WebGLPrograms.getParameters() determines required shader features
  4. Fragment shaders evaluate lighting using lights_fragment_begin and lights_physical_fragment
  5. Shadow sampling modulates light contributions using getShadow() functions

Key Components:

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:

MethodPurposeCalled From
setup(lights)Process lights, populate uniform arrays, update version hashWebGLRenderer.render() before shader compilation
setupView(lights, camera)Transform light properties to camera view spaceWebGLRenderer.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

SVG
100%

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

SVG
100%

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

SVG
100%

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 TypeUniform StructureProperties
AmbientambientLightColor: []RGB color array
Light ProbeslightProbe: []9 Vec3 coefficients for spherical harmonics
DirectionaldirectionalLights: []{direction, color} per light
Directional ShadowsdirectionalLightShadows: []{shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize}
directionalShadowMap: []Texture array
directionalShadowMatrix: []Matrix4 array
SpotspotLights: []{color, position, direction, distance, coneCos, penumbraCos, decay}
Spot ShadowsspotLightShadows: []Shadow parameters (same as directional)
spotLightMap: []Cookie/projector textures
spotLightMatrix: []Light space transforms
spotShadowMap: []Shadow textures
PointpointLights: []{color, position, decay, distance}
Point ShadowspointLightShadows: []{shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize, shadowCameraNear, shadowCameraFar}
pointShadowMap: []Cube texture array
pointShadowMatrix: []Matrix4 array
HemispherehemisphereLights: []{direction, skyColor, groundColor}
Rect ArearectAreaLights: []{color, position, width, height}
ltc_1, ltc_2: nullLinearly Transformed Cosines lookup tables

Shadow System

Shadow Map Types

Three.js supports multiple shadow mapping algorithms, each with different quality/performance tradeoffs:

Shadow TypeConstantSampler TypeFilteringDescription
PCF Shadow MapPCFShadowMapsampler2DShadowLinear (hardware PCF)Percentage Closer Filtering using hardware comparison
Basic Shadow Map(default fallback)sampler2DNearestSimple depth comparison, no filtering
VSM Shadow MapVSMShadowMapsampler2D (RG format)Gaussian blurVariance 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

SVG
100%

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)

SVG
100%

Point Light Shadow Maps

Point lights require capturing depth in all directions, using a cube render target:

SVG
100%

Shadow Rendering Pipeline

WebGLShadowMap.render() Execution Flow

SVG
100%

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.

SVG
100%

Shadow Side Mapping [line 51]:

The shadowSide object at src/renderers/webgl/WebGLShadowMap.js51 inverts face culling for shadow casting:

Original SideShadow SideRationale
FrontSideBackSideFront faces cast shadows from their back sides
BackSideFrontSideBack faces cast shadows from their front sides
DoubleSideDoubleSideBoth 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:

SVG
100%

Filtering Criteria:

  • object.visible === true
  • object.layers.test(camera.layers) passes
  • Object is Mesh, Line, or Points
  • object.castShadow === true OR (object.receiveShadow === true AND type === VSMShadowMap)
  • Not frustum culled OR _frustum.intersectsObject(object) returns true

Rendering Steps:

  1. Update object.modelViewMatrix = shadowCamera.matrixWorldInverse * object.matrixWorld [line 517]
  2. Get geometry via objects.update(object) [line 519]
  3. For array materials, loop through geometry.groups and render each [lines 524-543]
  4. Call object.onBeforeShadow() [line 535, 549]
  5. Call renderer.renderBufferDirect() with depth material [line 537, 551]
  6. Call object.onAfterShadow() [line 539, 553]
  7. 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

SVG
100%

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_PASS define [line 69]
  • Both use shader code from src/renderers/shaders/ShaderLib/vsm.glsl.js
  • Configurable VSM_SAMPLES define 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_PCF

Lighting Shader Chunks

Diagram: Lighting Shader Integration Flow

SVG
100%

Key Shader Chunks:

ChunkPurposeLocation
lights_pars_beginLight uniform declarations and helper functionssrc/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
lights_physical_pars_fragmentPBR BRDF functions and material structsrc/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js
lights_fragment_beginInitialize geometry vectors, iterate lightssrc/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
lights_fragment_endCompute indirect lightingsrc/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js
shadowmap_pars_vertexShadow matrix uniforms and varyingssrc/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js
shadowmap_vertexCompute shadow coordinatessrc/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js
shadowmap_pars_fragmentShadow sampling functionssrc/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:

FunctionPurposeLines
V_GGX_SmithCorrelated()GGX visibility term (geometry function)76-83
D_GGX()GGX normal distribution function89-96
BRDF_GGX()Main GGX specular BRDF155-200
BRDF_GGX_Clearcoat()Clearcoat layer BRDF128-151
BRDF_Sheen()Sheen (fabric) BRDF using Charlie distribution344-356
EnvironmentBRDF()Image-based lighting using DFG LUT379-385
computeMultiscattering()Energy-conserving multiscattering392-420
LTC_Evaluate()Linearly Transformed Cosines for area lights254-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

SVG
100%

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:

  1. Looks up cache using _materialCache[baseUuid][materialUuid]
  2. On cache miss, clones the base material
  3. Stores clone in cache
  4. Adds dispose event 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 update

Light 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 blur

Object 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