灯光与阴影
概述
Three.js 实现了完整的灯光和阴影管线,结合了动态灯光评估与阴影贴图遮挡测试。系统由两个主要子系统组成:
灯光系统(WebGLLights):
- 从场景中收集和组织灯光
- 将灯光属性转换为着色器 uniform
- 支持方向光、点光源、聚光灯、半球光和矩形区域光
- 处理基于图像的照明的光照探针
阴影系统(WebGLShadowMap):
- 从每个灯光的视角将场景渲染到深度纹理中
- 支持 PCF(百分比更近过滤)和 VSM(方差阴影贴图)
- 在顶点着色器中生成阴影坐标
- 在片元着色期间采样阴影贴图以确定遮挡
渲染管线:
WebGLLights.setup()处理灯光并填充 uniform 数组WebGLShadowMap.render()生成阴影贴图(如果启用)WebGLPrograms.getParameters()确定所需的着色器功能- 片元着色器使用
lights_fragment_begin和lights_physical_fragment评估灯光 - 阴影采样使用
getShadow()函数调制灯光贡献
关键组件:
WebGLLights:灯光数据处理,位于 src/renderers/webgl/WebGLLights.js157-581WebGLShadowMap:阴影贴图生成,位于 src/renderers/webgl/WebGLShadowMap.js35-597UniformsLib.lights:灯光 uniform 定义,位于 src/renderers/shaders/UniformsLib.js119-197lights_physical_pars_fragment:PBR 灯光方程,位于 src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js
灯光系统架构
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 处的 UniformsCache 和 ShadowUniformsCache 类为每个灯光创建并缓存 uniform 对象以避免重复分配:
图表:灯光 Uniform 缓存架构
每种灯光类型都有一个特定的 uniform 结构,创建一次并重复使用。缓存以 light.id 为键 [lines 16, 91]。
灯光设置过程
src/renderers/webgl/WebGLLights.js212-489 处的 setup() 方法处理场景的灯光并填充 uniform 数组:
图表:灯光设置流程
灯光排序 [line 233]:灯光通过 shadowCastingAndTexturingLightsFirst() [lines 151-155] 排序,以确保首先处理投射阴影的灯光,优化着色器 uniform 数组索引。
视图空间转换
src/renderers/webgl/WebGLLights.js491-573 处的 setupView() 方法将灯光属性从世界空间转换到摄像机视图空间:
图表:按灯光类型的视图空间转换
为什么使用视图空间? 将灯光转换到视图空间简化了着色器计算——摄像机位于原点向下看 -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。
阴影架构
组件图
渲染目标创建
阴影贴图存储在根据灯光类型和阴影贴图算法而不同的渲染目标中:
标准阴影贴图(方向光/聚光灯)
点光源阴影贴图
点光源需要捕获所有方向的深度,使用立方体渲染目标:
阴影渲染管线
WebGLShadowMap.render() 执行流程
深度材质生成
src/renderers/webgl/WebGLShadowMap.js418-505 处的 getDepthMaterial() 函数为阴影渲染选择或创建适当的材质。材质按(基础材质、原始材质)对进行缓存以避免重复克隆。
阴影面映射 [line 51]:
src/renderers/webgl/WebGLShadowMap.js51 处的 shadowSide 对象为阴影投射反转面剔除:
|| 原始面 | 阴影面 | 原理 | ||---------------|--------------|------------------------------------------------| || FrontSide | BackSide | 前面从其背面投射阴影 | || BackSide | FrontSide | 背面从其前面投射阴影 | || DoubleSide | DoubleSide | 两面都投射阴影 |
对于 VSM 阴影贴图,材质的 shadowSide 或 side 直接使用而不进行反转 [lines 471-479](<https://github.com/mrdoob/three.js/blob/0a17afda/lines 471-479>)
场景遍历和对象渲染
src/renderers/webgl/WebGLShadowMap.js507-568 处的 renderObject() 函数递归遍历场景图以渲染投射阴影的对象:
过滤标准:
object.visible === trueobject.layers.test(camera.layers)通过- 对象是
Mesh、Line或Points object.castShadow === true或(object.receiveShadow === true且type === VSMShadowMap)- 未被视锥体剔除 或
_frustum.intersectsObject(object)返回 true
渲染步骤:
- 更新
object.modelViewMatrix = shadowCamera.matrixWorldInverse * object.matrixWorld[line 517] - 通过
objects.update(object)获取几何体 [line 519] - 对于数组材质,遍历
geometry.groups并渲染每个 [lines 524-543] - 调用
object.onBeforeShadow()[line 535, 549] - 使用深度材质调用
renderer.renderBufferDirect()[line 537, 551] - 调用
object.onAfterShadow()[line 539, 553] - 递归处理
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
用于模糊通道的全屏三角形网格:
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灯光着色器块
图表:灯光着色器集成流程
关键着色器块:
|| 块 | 目的 | 位置 | ||---------------------------------|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| || 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 着色器块为 MeshStandardMaterial 和 MeshPhysicalMaterial 实现基于物理的渲染方程。
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 块遍历所有活动灯光并累积它们的贡献:
图表:片元着色器灯光循环
循环使用 #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() 确定需要自定义时,它:
- 使用
_materialCache[baseUuid][materialUuid]查找缓存 - 在缓存未命中时,克隆基础材质
- 将克隆存储在缓存中
- 为清理向原始材质添加
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; // 用于点光源