Skip to content

灯光与阴影

概述

Three.js 实现了完整的灯光和阴影管线,结合了动态灯光评估与阴影贴图遮挡测试。系统由两个主要子系统组成:

灯光系统(WebGLLights):

  • 从场景中收集和组织灯光
  • 将灯光属性转换为着色器 uniform
  • 支持方向光、点光源、聚光灯、半球光和矩形区域光
  • 处理基于图像的照明的光照探针

阴影系统(WebGLShadowMap):

  • 从每个灯光的视角将场景渲染到深度纹理中
  • 支持 PCF(百分比更近过滤)和 VSM(方差阴影贴图)
  • 在顶点着色器中生成阴影坐标
  • 在片元着色期间采样阴影贴图以确定遮挡

渲染管线:

  1. WebGLLights.setup() 处理灯光并填充 uniform 数组
  2. WebGLShadowMap.render() 生成阴影贴图(如果启用)
  3. WebGLPrograms.getParameters() 确定所需的着色器功能
  4. 片元着色器使用 lights_fragment_beginlights_physical_fragment 评估灯光
  5. 阴影采样使用 getShadow() 函数调制灯光贡献

关键组件:

灯光系统架构

WebGLLights 概述

src/renderers/webgl/WebGLLights.js157-581 处的 WebGLLights 类管理从场景对象到着色器 uniform 的灯光数据转换。它维护内部状态并提供两个主要方法:

|| 方法 | 目的 | 调用自 | ||-----------------------------|--------------------------------------------------------------|----------------------------------------------------| || setup(lights) | 处理灯光,填充 uniform 数组,更新版本哈希 | WebGLRenderer.render() 在着色器编译之前 | || setupView(lights, camera) | 将灯光属性转换到摄像机视图空间 | WebGLRenderer.render() 在渲染之前 |

状态结构 [lines 163-204]:

state = {
    version: 0,                    // 灯光配置更改时递增
    hash: { ... },                 // 跟踪灯光数量以检测更改
    ambient: [r, g, b],           // 组合的环境光颜色
    probe: [Vec3 × 9],            // 光照探针的球谐函数

    // 每种灯光类型的数组
    directional: [],              // DirectionalLight uniform
    directionalShadow: [],        // 阴影参数
    directionalShadowMap: [],     // 阴影纹理
    directionalShadowMatrix: [],  // 世界到阴影的变换

    spot: [],                     // SpotLight uniform
    spotLightMap: [],             // 光照 cookie/投影仪
    spotLightMatrix: [],          // 光照空间变换
    spotShadow: [],               // 阴影参数
    spotShadowMap: [],            // 阴影纹理

    point: [],                    // PointLight uniform
    pointShadow: [],              // 阴影参数
    pointShadowMap: [],           // 立方体阴影纹理
    pointShadowMatrix: [],        // 阴影变换

    rectArea: [],                 // RectAreaLight uniform
    rectAreaLTC1: null,           // LTC 查找表 1
    rectAreaLTC2: null,           // LTC 查找表 2

    hemi: [],                     // HemisphereLight uniform
    numSpotLightShadowsWithMaps: 0,
    numLightProbes: 0
}

灯光 Uniform 缓存

src/renderers/webgl/WebGLLights.js8-145 处的 UniformsCacheShadowUniformsCache 类为每个灯光创建并缓存 uniform 对象以避免重复分配:

图表:灯光 Uniform 缓存架构

SVG
100%

每种灯光类型都有一个特定的 uniform 结构,创建一次并重复使用。缓存以 light.id 为键 [lines 16, 91]。

灯光设置过程

src/renderers/webgl/WebGLLights.js212-489 处的 setup() 方法处理场景的灯光并填充 uniform 数组:

图表:灯光设置流程

SVG
100%

灯光排序 [line 233]:灯光通过 shadowCastingAndTexturingLightsFirst() [lines 151-155] 排序,以确保首先处理投射阴影的灯光,优化着色器 uniform 数组索引。

视图空间转换

src/renderers/webgl/WebGLLights.js491-573 处的 setupView() 方法将灯光属性从世界空间转换到摄像机视图空间:

图表:按灯光类型的视图空间转换

SVG
100%

为什么使用视图空间? 将灯光转换到视图空间简化了着色器计算——摄像机位于原点向下看 -Z 轴,使灯光方向和位置计算更高效。

灯光 Uniform 结构

src/renderers/shaders/UniformsLib.js119-197 处的 UniformsLib.lights 对象为所有灯光类型定义着色器 uniform 结构:

|| 灯光类型 | Uniform 结构 | 属性 | ||---------------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------| || 环境光 | ambientLightColor: [] | RGB 颜色数组 | || 光照探针 | lightProbe: [] | 球谐函数的 9 个 Vec3 系数 | || 方向光 | directionalLights: [] | 每个灯光的 {direction, color} | || 方向光阴影 | directionalLightShadows: [] | {shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize} | || | directionalShadowMap: [] | 纹理数组 | || | directionalShadowMatrix: [] | Matrix4 数组 | || 聚光灯 | spotLights: [] | {color, position, direction, distance, coneCos, penumbraCos, decay} | || 聚光灯阴影 | spotLightShadows: [] | 阴影参数(与方向光相同) | || | spotLightMap: [] | Cookie/投影仪纹理 | || | spotLightMatrix: [] | 光照空间变换 | || | spotShadowMap: [] | 阴影纹理 | || 点光源 | pointLights: [] | {color, position, decay, distance} | || 点光源阴影 | pointLightShadows: [] | {shadowIntensity, shadowBias, shadowNormalBias, shadowRadius, shadowMapSize, shadowCameraNear, shadowCameraFar} | || | pointShadowMap: [] | 立方体纹理数组 | || | pointShadowMatrix: [] | Matrix4 数组 | || 半球光 | hemisphereLights: [] | {direction, skyColor, groundColor} | || 矩形区域光 | rectAreaLights: [] | {color, position, width, height} | || | ltc_1, ltc_2: null | 线性变换余弦查找表 |

阴影系统

阴影贴图类型

Three.js 支持多种阴影贴图算法,每种算法具有不同的质量/性能权衡:

|| 阴影类型 | 常量 | 采样器类型 | 过滤 | 描述 | ||-----------------------|--------------------|-------------------------|-----------------------|-------------------------------------------------------| || PCF 阴影贴图 | PCFShadowMap | sampler2DShadow | 线性(硬件 PCF) | 使用硬件比较的百分比更近过滤 | || 基本阴影贴图 | (默认回退) | sampler2D | 最近 | 简单的深度比较,无过滤 | || VSM 阴影贴图 | VSMShadowMap | sampler2D(RG 格式) | 高斯模糊 | 具有两通道模糊的方差阴影贴图 | || PCF Soft(已弃用) | PCFSoftShadowMap | - | - | 已弃用,回退到 PCF |

阴影贴图类型在渲染器上设置:renderer.shadowMap.type = PCFShadowMap

阴影架构

组件图

SVG
100%

渲染目标创建

阴影贴图存储在根据灯光类型和阴影贴图算法而不同的渲染目标中:

标准阴影贴图(方向光/聚光灯)

SVG
100%

点光源阴影贴图

点光源需要捕获所有方向的深度,使用立方体渲染目标:

SVG
100%

阴影渲染管线

WebGLShadowMap.render() 执行流程

SVG
100%

深度材质生成

src/renderers/webgl/WebGLShadowMap.js418-505 处的 getDepthMaterial() 函数为阴影渲染选择或创建适当的材质。材质按(基础材质、原始材质)对进行缓存以避免重复克隆。

SVG
100%

阴影面映射 [line 51]:

src/renderers/webgl/WebGLShadowMap.js51 处的 shadowSide 对象为阴影投射反转面剔除:

|| 原始面 | 阴影面 | 原理 | ||---------------|--------------|------------------------------------------------| || FrontSide | BackSide | 前面从其背面投射阴影 | || BackSide | FrontSide | 背面从其前面投射阴影 | || DoubleSide | DoubleSide | 两面都投射阴影 |

对于 VSM 阴影贴图,材质的 shadowSideside 直接使用而不进行反转 [lines 471-479](<https://github.com/mrdoob/three.js/blob/0a17afda/lines 471-479>)

场景遍历和对象渲染

src/renderers/webgl/WebGLShadowMap.js507-568 处的 renderObject() 函数递归遍历场景图以渲染投射阴影的对象:

SVG
100%

过滤标准:

  • object.visible === true
  • object.layers.test(camera.layers) 通过
  • 对象是 MeshLinePoints
  • object.castShadow === true 或(object.receiveShadow === truetype === VSMShadowMap
  • 未被视锥体剔除 或 _frustum.intersectsObject(object) 返回 true

渲染步骤:

  1. 更新 object.modelViewMatrix = shadowCamera.matrixWorldInverse * object.matrixWorld [line 517]
  2. 通过 objects.update(object) 获取几何体 [line 519]
  3. 对于数组材质,遍历 geometry.groups 并渲染每个 [lines 524-543]
  4. 调用 object.onBeforeShadow() [line 535, 549]
  5. 使用深度材质调用 renderer.renderBufferDirect() [line 537, 551]
  6. 调用 object.onAfterShadow() [line 539, 553]
  7. 递归处理 object.children [lines 561-567]

方差阴影贴图(VSM)

方差阴影贴图在 RGFormat 纹理中存储深度矩(均值和方差),对其进行模糊以实现软阴影并减少光泄漏。src/renderers/webgl/WebGLShadowMap.js375-416 处的 VSMPass() 函数执行两通道可分离高斯模糊:

通道 1(垂直):从 shadow.map.depthTexture 读取 → 写入到 shadow.mapPass 通道 2(水平):从 shadow.mapPass.texture 读取 → 写入到 shadow.map

SVG
100%

用于模糊通道的全屏三角形网格:

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);

这个超大三角形(顶点位于 (-1,-1)、(3,-1)、(-1,3))在渲染时覆盖整个视口。

着色器材质:

  • shadowMaterialVertical:垂直模糊通道 [line 53]
  • shadowMaterialHorizontal:水平模糊通道,HORIZONTAL_PASS 定义 [line 69]
  • 两者都使用 src/renderers/shaders/ShaderLib/vsm.glsl.js 的着色器代码
  • 可配置的 VSM_SAMPLES 定义控制模糊质量 [lines 54-55, 379-386]

全屏三角形:

// 创建覆盖整个视口的超大三角形 [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
    )
);

位于 (-1,-1)、(3,-1)、(-1,3) 的顶点确保三角形在渲染时覆盖完整的 NDC 空间。

着色器集成

灯光的程序参数

src/renderers/webgl/WebGLPrograms.js50-381 处的 WebGLPrograms.getParameters() 函数根据场景的灯光配置生成着色器定义:

灯光数量参数 [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
}

阴影参数 [lines 344-345]:

{
    shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
    shadowMapType: renderer.shadowMap.type
}

这些参数由 src/renderers/webgl/WebGLProgram.js214-231 处的 replaceLightNums() 处理,以将定义注入着色器代码:

#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

灯光着色器块

图表:灯光着色器集成流程

SVG
100%

关键着色器块:

|| 块 | 目的 | 位置 | ||---------------------------------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| || lights_pars_begin | 灯光 uniform 声明和辅助函数 | src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js | || lights_physical_pars_fragment | PBR BRDF 函数和材质结构 | src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js | || lights_fragment_begin | 初始化几何体向量,遍历灯光 | src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js | || lights_fragment_end | 计算间接照明 | src/renderers/shaders/ShaderChunk/lights_fragment_end.glsl.js | || shadowmap_pars_vertex | 阴影矩阵 uniform 和 varying | src/renderers/shaders/ShaderChunk/shadowmap_pars_vertex.glsl.js | || shadowmap_vertex | 计算阴影坐标 | src/renderers/shaders/ShaderChunk/shadowmap_vertex.glsl.js | || shadowmap_pars_fragment | 阴影采样函数 | src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js |

基于物理的灯光

src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js1-612 处的 lights_physical_pars_fragment 着色器块为 MeshStandardMaterialMeshPhysicalMaterial 实现基于物理的渲染方程。

PhysicalMaterial 结构 [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
};

核心 BRDF 函数:

|| 函数 | 目的 | 行 | ||----------------------------|------------------------------------------------|---------| || V_GGX_SmithCorrelated() | GGX 可见性项(几何函数) | 76-83 | || D_GGX() | GGX 法线分布函数 | 89-96 | || BRDF_GGX() | 主 GGX 镜面反射 BRDF | 155-200 | || BRDF_GGX_Clearcoat() | 清漆层 BRDF | 128-151 | || BRDF_Sheen() | Sheen(织物)使用 Charlie 分布的 BRDF | 344-356 | || EnvironmentBRDF() | 使用 DFG LUT 的基于图像的照明 | 379-385 | || computeMultiscattering() | 能量守恒的多散射 | 392-420 | || LTC_Evaluate() | 区域光的线性变换余弦 | 254-315 |

直接照明函数 [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
        // 清漆镜面反射贡献
        clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat(...);
    #endif

    #ifdef USE_SHEEN
        sheenSpecularDirect += irradiance * BRDF_Sheen(...);
        // Sheen 的能量补偿
        irradiance *= sheenEnergyComp;
    #endif

    // 带多散射的镜面反射
    reflectedLight.directSpecular += irradiance * BRDF_GGX_Multiscatter(...);

    // 漫反射(朗伯)
    reflectedLight.directDiffuse += irradiance * BRDF_Lambert(material.diffuseContribution);
}

间接照明函数 [lines 563-610]:

void RE_IndirectDiffuse_Physical(...) {
    vec3 diffuse = irradiance * BRDF_Lambert(material.diffuseContribution);

    #ifdef USE_SHEEN
        // Sheen 能量补偿
        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

    // 计算电介质和金属贡献的多散射
    computeMultiscatteringIridescence(..., singleScatteringDielectric, multiScatteringDielectric);
    computeMultiscatteringIridescence(..., singleScatteringMetallic, multiScatteringMetallic);

    // 基于金属度混合
    vec3 singleScattering = mix(singleScatteringDielectric, singleScatteringMetallic, material.metalness);
    vec3 multiScattering = mix(multiScatteringDielectric, multiScatteringMetallic, material.metalness);

    reflectedLight.indirectSpecular += radiance * singleScattering;
    reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;
}

片元着色器中的灯光迭代

src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js1-205 处的 lights_fragment_begin 块遍历所有活动灯光并累积它们的贡献:

图表:片元着色器灯光循环

SVG
100%

循环使用 #pragma unroll_loop_start / #pragma unroll_loop_end 指令在编译时展开循环。UNROLLED_LOOP_INDEX 宏提供循环计数器用于索引阴影数组。

深度材质着色器

两种专门的材质处理阴影通道期间的深度渲染:

MeshDepthMaterial - 方向光/聚光灯

用于方向光和聚光灯。渲染从摄像机近平面到远平面线性映射的场景深度。创建一次并在 src/renderers/webgl/WebGLShadowMap.js44 处重用

着色器定义 位于 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
}

片元着色器输出标准化深度:gl_FragDepth = (mvPosition.z + cameraNear) / (cameraFar - cameraNear)

MeshDistanceMaterial - 点光源

用于点光源。计算从片元到灯光位置的距离,将其编码为深度以用于全向阴影贴图。创建一次并在 src/renderers/webgl/WebGLShadowMap.js45 处重用

着色器定义 位于 src/renderers/shaders/ShaderLib.js270-284

distance: {
    uniforms: mergeUniforms([
        UniformsLib.common,
        UniformsLib.displacementmap,
        {
            referencePosition: { value: new Vector3() },  // 灯光世界位置
            nearDistance: { value: 1 },                   // 灯光近裁剪面
            farDistance: { value: 1000 }                  // 灯光远裁剪面
        }
    ]),
    vertexShader: ShaderChunk.distance_vert,
    fragmentShader: ShaderChunk.distance_frag
}

referencePosition uniform 在 src/renderers/webgl/WebGLMaterials.js498-500 处的 WebGLMaterials 中设置为点光源的世界位置。片元着色器计算:

float dist = length(vWorldPosition - referencePosition);
gl_FragColor = packDepthToRGBA((dist - nearDistance) / (farDistance - nearDistance));

材质缓存管理

为了避免为需要自定义深度材质的对象(例如带有裁剪、置换或 alpha)重复克隆材质,系统在 src/renderers/webgl/WebGLShadowMap.js47 处维护两级缓存:

_materialCache = {
    [baseMaterialUuid]: {         // _depthMaterial.uuid 或 _distanceMaterial.uuid
        [objectMaterialUuid]: cachedCustomMaterial
    }
}

缓存填充 [lines 440-462]:当 getDepthMaterial() 确定需要自定义时,它:

  1. 使用 _materialCache[baseUuid][materialUuid] 查找缓存
  2. 在缓存未命中时,克隆基础材质
  3. 将克隆存储在缓存中
  4. 为清理向原始材质添加 dispose 事件监听器

缓存清理 [lines 571-594]:当任何材质被释放时调用 onMaterialDispose() 函数:

function onMaterialDispose(event) {
    const material = event.target;
    material.removeEventListener('dispose', onMaterialDispose);

    // 遍历所有缓存的材质
    for (const id in _materialCache) {
        const cache = _materialCache[id];
        if (material.uuid in cache) {
            const shadowMaterial = cache[material.uuid];
            shadowMaterial.dispose();    // 释放阴影材质
            delete cache[material.uuid]; // 从缓存中移除
        }
    }
}

这确保当释放具有自定义阴影材质的材质时,所有关联的阴影材质克隆也被正确释放,以防止内存泄漏。

配置和使用

阴影映射在多个级别进行配置:

渲染器级别

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFShadowMap;  // 或 VSMShadowMap
renderer.shadowMap.autoUpdate = true;     // 每帧自动更新
renderer.shadowMap.needsUpdate = true;    // 强制单次更新

灯光级别

每个有阴影的灯光都有一个 shadow 属性:

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;  // 用于 VSM 模糊

对象级别

各个对象控制阴影行为:

object.castShadow = true;     // 对象投射阴影
object.receiveShadow = true;  // 对象接收阴影

// 用于特殊渲染的自定义深度材质
object.customDepthMaterial = myDepthMaterial;      // 用于方向光/聚光灯
object.customDistanceMaterial = myDistanceMaterial; // 用于点光源