three.js 光照阴影配置完全指南:深入解析 LightShadow 基类及其渲染原理
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
LightShadow是 three.js 中所有光照阴影配置类的抽象基类,负责描述"一盏灯投出的阴影"应当如何渲染——从阴影贴图分辨率、深度偏差、强度到地图类型。本指南以 LightShadow 官方文档 为骨架,结合 LightShadow.js 源码、WebGLShadowMap.js 渲染实现 及其单元测试,系统讲解该类全部属性与方法、三种内置子类的差异,以及 WebGL / WebGPU 两套渲染管线中的实际工作原理,帮助你诊断与消除阴影瑕疵、按需配置出高质量阴影。
LightShadow 是什么:所有光源阴影的公共配置层
在 three.js 中,平行光(DirectionalLight)、聚光灯(SpotLight)和点光源(PointLight)各自持有一个shadow属性对象,类型分别为DirectionalLightShadow、SpotLightShadow和PointLightShadow。这三个类全部继承自同一个抽象基类LightShadow:
- DirectionalLightShadow —— 继承
LightShadow,表示平行光的阴影配置; - SpotLightShadow —— 继承
LightShadow,表示聚光灯的阴影配置; - PointLightShadow —— 继承
LightShadow,表示点光源的阴影配置。
三个子类的继承关系可以通过单元测试得到验证,例如 DirectionalLightShadow.tests.js 中断言DirectionalLightShadow是LightShadow的实例。从源码结构看,这三个子类的差异主要集中在两点:内建阴影相机(shadow camera)的类型与参数,以及对阴影矩阵更新逻辑的覆写。光照本体只需在构造时创建对应的阴影实例,DirectionalLight.js 中this.shadow = new DirectionalLightShadow()、SpotLight.js 与 PointLight.js 中同理。
LightShadow自身在 LightShadow.js 第 18 行 被声明为抽象基类(@abstract),因此你不会直接new LightShadow(camera),而是通过具体光源的shadow属性操作它。它的职责是:把一个抽象"相机视角的深度图"(shadow map)与一组渲染参数封装成渲染器可以直接消费的对象。
一份典型的阴影开启流程
在动手调参前,先回顾三个性 shadow 生效的基本前提(完整示例见 examples/webgl_shadowmap.html):
// 1. 让光源投影 const light = new THREE.DirectionalLight( 0xffffff, 1 ); light.castShadow = true; // 光本身声明"我要投影" light.shadow.mapSize.width = 1024; // 通过 light.shadow 配置阴影贴图 light.shadow.mapSize.height = 1024; // 2. 开启渲染器的阴影系统 renderer.shadowMap.enabled = true; // renderer.shadowMap 即 WebGLShadowMap renderer.shadowMap.type = THREE.PCFShadowMap; // 3. 让物体参与 mesh.castShadow = true; // 投射阴影 ground.receiveShadow = true; // 接收阴影只有三者同时满足,LightShadow上配置的参数才会真正参与渲染。注意light.shadow(LightShadow 实例)与renderer.shadowMap(全局阴影模块)是两层不同的概念:前者控制"这盏灯的影子长什么样",后者控制"整个场景用什么算法渲染影子"。
构造函数:光源眼中的世界
new LightShadow( camera : Camera )(抽象)
构造一个新的 light shadow 实例。
| 参数 | 说明 |
|---|---|
camera | 光源观察世界的视角(light's view of the world),即用于从光源位置渲染深度图的内部相机。 |
这个相机随后暴露为公共属性shadow.camera,是理解阴影贴图最重要的对象。三个内置子类在构造时传入了不同的相机:
| 子类 | 内建相机 | 默认参数(源码出处) |
|---|---|---|
DirectionalLightShadow | OrthographicCamera | ( -5, 5, 5, -5, 0.5, 500 ),见 DirectionalLightShadow.js |
SpotLightShadow | PerspectiveCamera | ( 50, 1, 0.5, 500 ),见 SpotLightShadow.js |
PointLightShadow | PerspectiveCamera | ( 90, 1, 0.5, 500 ),见 PointLightShadow.js |
这个设计非常直观:平行光近似无限远、光线平行,所以用正交相机框定一个长方体视景体(left/right/top/bottom决定投影范围,可通过light.shadow.camera.left等调整);聚光灯和点光源从一点向四周/锥形辐射,所以用透视相机。其中点光源需要渲染六张面(立方体贴图),其相机默认视场角 90° 也暗示了这一点。
子类在构造时追加的专属属性
DirectionalLightShadow额外定义了只读标志isDirectionalLightShadow = true,用于运行时类型判断(源码);PointLightShadow同理带有isPointLightShadow(源码);SpotLightShadow除isSpotLightShadow外还新增了两个可调参数(源码):focus:用于聚焦阴影相机。相机的视场角被设定为聚光灯视场角的focus百分比,取值范围[0, 1],默认1。调小focus会让阴影聚焦到聚光锥中心区域,从而提高贴图有效分辨率;aspect:阴影贴图的宽高比,默认1。
SpotLightShadow.updateMatrices()每次渲染都会依据light.angle × focus、mapSize宽高比与light.distance(或相机 far)实时校正相机参数并调用updateProjectionMatrix()(见 SpotLightShadow.js),因此聚光灯阴影相机是"自适应"的。
属性全景:从分辨率到偏差的每个旋钮
LightShadow构造器在 LightShadow.js 第 25-171 行 中一次性初始化了全部属性。下表汇总所有公共属性及其默认值:
| 属性 | 类型 | 默认值 | 一句话作用 |
|---|---|---|---|
camera | Camera | 子类决定 | 光源视角相机(决定贴图内容与范围) |
intensity | number | 1 | 阴影强度,有效范围[0, 1] |
bias | number | 0 | 深度偏差,消除阴影痤疮(shadow acne) |
biasNode | Node<float> | null | bias的节点化版本,仅 WebGPU 节点管线生效 |
normalBias | number | 0 | 沿法线方向偏移采样位置,用于浅角度大面积去痤疮 |
radius | number | 1 | PCF 模糊半径;对BasicShadowMap无效 |
blurSamples | number | 8 | VSM 阴影模糊采样数 |
mapSize | Vector2 | (512, 512) | 阴影贴图宽高(必须为 2 的幂) |
mapType | number | UnsignedByteType | 阴影纹理数据类型 |
map | RenderTarget | null | 内部相机生成的深度图 |
mapPass | RenderTarget | null | 内部相机生成的分布图(VSM 中间产物) |
matrix | Matrix4 | 单位阵 | 模型到阴影相机空间的变换矩阵 |
autoUpdate | boolean | true | 是否在渲染循环中自动更新阴影贴图 |
needsUpdate | boolean | false | 置true后在下一次render时强制更新 |
camera:光源视角
即构造器传入的相机。你可以直接访问light.shadow.camera调整其视景体参数来"裁剪"阴影覆盖范围,例如:
light.shadow.camera.left = -10; light.shadow.camera.right = 10; light.shadow.camera.top = 10; light.shadow.camera.bottom = -10; light.shadow.camera.updateProjectionMatrix(); // 修改近远裁剪面后通常需要调用mapSize:质量与开销的权衡旋钮
类型Vector2,定义阴影贴图宽高。数值越高阴影质量越好,但计算与内存开销越大;且值必须是 2 的幂(这是 GPU 纹理的硬性要求)。默认(512, 512),实用中常见配置为1024、2048或4096。渲染器会受capabilities.maxTextureSize上限约束——当配置超出设备上限时,WebGLShadowMap.js 会把mapSize自动钳制到设备允许的最大值。
bias 与 normalBias:对抗两种典型阴影瑕疵
bias(默认0):决定判定"表面是否处于阴影"时,从归一化深度上增减多少。它用于抵消阴影痤疮(shadow acne)——当表面与光方向几乎平行时,深度图自身量化误差会让表面在明暗间闪烁。这里的调整量通常非常微小(约0.0001量级),过大会导致阴影整体偏移(即 peter-panning,物体"飘"起来)。
normalBias(默认0):将用于查询阴影贴图的位置沿物体法线方向偏移。增大它可显著缓解大面积场景中浅角度入射光造成的痤疮,代价是阴影可能略微形变。因为 normalBias 是几何方向性偏移,它不会像 bias 那样产生深度错位,因此在现代 three.js 实践中,优先增大normalBias而非bias是更常见的做法。
biasNode(默认null):bias的节点化版本,只在 WebGPURenderer 的节点(TSL)渲染管线中受支持。在节点实现 ShadowNode.js 第 285 行 中可以看到取舍逻辑:
const bias = shadow.biasNode || reference( 'bias', 'float', shadow ).setGroup( renderGroup );即:一旦定义了biasNode,普通的数值bias便不再生效。这让你可以把偏差值做进可动画、可程序化生成的 TSL 表达式里。同时 ShadowNode.js 第 314 行 表明,当渲染器启用reversedDepthBuffer(反向深度缓冲)时偏差是"减去"而非"加上"。
intensity:阴影的浓淡
阴影强度,默认1,有效范围[0, 1]。调低它可以让阴影变淡(例如模拟多云天气的半影环境),0则几乎无阴影。
map 与 mapPass:渲染期自动生成的贴图对象
map:使用内部相机生成的深度图,任何"比该像素深度更远"的位置即处于阴影中。默认null,由渲染器在渲染过程中惰性创建并填充;mapPass:使用内部相机生成的分布图,遮蔽程度基于深度分布统计计算(Variance Shadow Map 的思路)。默认null,同样由渲染器内部管理。
两者都应视为"内部状态",不应手动赋值。在 WebGL 路径下,WebGLShadowMap.js 按当前阴影贴图类型惰性创建map:VSMShadowMap使用 RG 格式、HalfFloatType的WebGLRenderTarget并附带FloatType原生深度纹理;普通PCFShadowMap使用带DepthTexture的 2D 渲染目标;点光源则创建WebGLCubeRenderTarget。mapPass仅作为 VSM 两次高斯模糊(竖直→水平)之间的中间缓冲出现(VSMPass 实现)。
matrix:模型到阴影相机空间的桥梁
从模型空间变换到"阴影相机空间"的矩阵,用来在采样阴影贴图时计算位置与深度。渲染期间由updateMatrices()内部算出,见下文方法节。
mapType:阴影纹理的数据类型
阴影纹理类型,默认UnsignedByteType(src/constants.js中值为 1009,见 constants.js)。它在节点管线中真正生效:WebGPU 渲染器在创建阴影渲染目标时执行shadowMap.texture.type = shadow.mapType(ShadowNode.js 第 342 行)。而传统 WebGL 路径在 VSM 模式下会内部改用HalfFloatType。
autoUpdate 与 needsUpdate:精细控制更新时机
autoUpdate(默认true):是否自动更新该光源的阴影。如果你的场景不包含动态光照/动态阴影(所有物体静止、灯光不动),可以把它设为false省掉逐帧重绘深度图的开销。
needsUpdate(默认false):当设为true,阴影贴图将在下一次render调用时更新。典型用法正是配合关闭autoUpdate的静态场景——在需要刷新(例如移动了一次灯)时手动置位并触发一次渲染:
dirLight.shadow.autoUpdate = false; // 静态场景,不再每帧更新 // ... 场景发生变化时: dirLight.shadow.needsUpdate = true; // 请求一次强制刷新 renderer.render( scene, camera ); // 本次渲染完成阴影重建,随后 needsUpdate 被清回 false注意:渲染器侧存在两级这样的开关。WebGLShadowMap.js 第 84-95 行 显示renderer.shadowMap自身也有enabled、autoUpdate、needsUpdate,渲染时先检查全局开关、再逐个光源判断shadow.autoUpdate === false && shadow.needsUpdate === false则跳过(第 170 行);渲染完成后该光源的needsUpdate会复位为false(第 370 行)。
radius 与 blurSamples:两套软阴影算法的模糊参数
radius(默认1):当使用 PCF/PCSS 类的阴影贴图类型时,radius > 1会模糊阴影边缘(实现 PCF 软阴影)。但过高的值会引起可见的带状伪影(banding);更大的贴图尺寸允许使用更大的radius而伪影不明显。该属性在BasicShadowMap类型下无效。传统 WebGL 管线中radius同时作为 VSM 模糊的半径 uniform 传入(WebGLShadowMap.js 第 413 行)。blurSamples(默认8):对VSM(Variance Shadow Map)阴影贴图进行模糊时的采样数量。采样数越多模糊越平滑,成本越高。WebGLShadowMap.js 第 386-394 行 显示采样数通过 shader 宏VSM_SAMPLES注入着色器,并在数值变化时触发材质重新编译。
阴影贴图类型(renderer.shadowMap.type)与 LightShadow 的协作
渲染器支持的阴影贴图算法在 src/constants.js 中定义为数值常量:
BasicShadowMap = 0:无滤波硬阴影,最快最粗糙(此时radius无效);PCFShadowMap = 1:百分比渐近滤波软阴影;PCFSoftShadowMap:从 three.js r186 起标记为已弃用(constants.js 第 73 行 的@deprecated注释);在 WebGLShadowMap.js 第 99-104 行 中,当类型设为它时会打印警告并自动回退为PCFShadowMap;VSMShadowMap = 3:基于方差统计的软阴影;其额外特性是所有接收阴影的物体会同时投射阴影(注释见 constants.js 第 79 行)。注意点光源不支持 VSM,WebGLShadowMap.js 第 218-225 行 会直接跳过并提示改用 PCF/Basic。
方法解析:渲染器在幕后都调用了什么
LightShadow的多数公开方法标注为"渲染器内部使用",理解它们能让你把握阴影渲染的完整调用链。
updateMatrices( light : Light )
更新阴影相机与阴影矩阵,渲染器内部调用。其逻辑(LightShadow.js 第 213-224 行)清晰展示了"阴影相机跟随光源"的原理:
- 从
light.matrixWorld提取世界坐标位置,赋值给阴影相机position; - 让阴影相机朝向
light.target.matrixWorld位置(即光源的目标点),执行lookAt并updateMatrixWorld(); - 调用私有
_updateMatrix()计算matrix与_frustum。
随后的_updateMatrix()(LightShadow.js 第 235-268 行)用projectionMatrix × matrixWorldInverse求投影-屏幕矩阵,据此重建视锥体(Frustum),再把裁剪空间 NDC 坐标线性映射到[0,1]纹理空间,得到最终阴影矩阵。从该实现还可以看到对 WebGPU / 反向深度缓冲的分支处理:正向深度时 Z 行用(0.5, 0.5)做半区间映射,而 WebGPU 或反向深度时 Z 直接保持[0,1]投影结果(源码注释也点明了这一点)。
getFrustum() : Frustum
返回阴影相机的视锥体。渲染器用它对投射阴影的物体做视锥剔除:在 WebGLShadowMap.js 第 526 行 中,只有通过object.intersectsFrustum(_frustum)检测的物体会被写进深度图,避免无效绘制。
getViewportCount() 与 getViewport( viewportIndex : number )
getViewportCount():渲染器用它获取"该阴影需要渲染多少个视口";getViewport( viewportIndex ):返回给定视口索引对应的视口定义(Vector4,元素为x,y,w,h)。
默认实现中_viewportCount = 1、_viewports = [ new Vector4( 0, 0, 1, 1 ) ](LightShadow.js 第 160-169 行),即一个阴影独占整张贴图。WebGLShadowMap.js 第 289 行 计算总渲染面数时对点光源取 6(立方体六个面)、其余取getViewportCount();第 343-352 行把视口换算成实际像素区域后按 viewport 渲染。从源码注释与多视口/_frameExtents结构可以推断,这一机制是为**阴影图集(shadow atlas)与级联阴影(CSM 等多层阴影贴图共享一张大纹理)**预留的扩展点——例如平行光可以按级联切分贴图。
getFrameExtents() : Vector2
返回帧范围(frame extents),默认(1, 1)。与多视口机制配套使用:mapSize乘上_frameExtents才是整张纹理的实际尺寸(见 WebGLShadowMap.js 第 174-178 行)。
clone() 与 copy( source : LightShadow )
copy( source ):把给定 light shadow 的值拷贝到当前实例,返回this。从 源码 可见它拷贝了camera(以clone()深拷贝方式)、intensity、bias、radius、autoUpdate、needsUpdate、normalBias、blurSamples、mapSize与biasNode;clone():返回一份浅拷贝语义的新实例,实现为new this.constructor().copy( this )(源码),因此对子类调用会得到同类型实例。
DirectionalLightShadow.tests.js 第 42-61 行 与 SpotLightShadow.tests.js 对clone/copy做了回归验证:新实例互不相等、clone后相等、修改mapSize后又不同、copy可恢复相等。
dispose()
释放本实例占用的 GPU 相关资源。当实例不再使用时务必调用。实现(LightShadow.js 第 297-311 行)会依次调用map与mapPass两个 RenderTarget 的dispose()。
toJSON() : Object
把 light shadow 序列化为 JSON。输出字段为intensity、bias、normalBias、radius、blurSamples、mapSize(数组形式)与camera(通过camera.toJSON( false ).object内嵌,并删除其中的matrix以避免冗余,见 LightShadow.js 第 358-374 行)。SpotLightShadow覆写后额外追加focus与aspect(SpotLightShadow.js 第 85-94 行)。它配合 ObjectLoader#parse 实现"存盘→还原"的完整回路——DirectionalLightShadow.tests.js 第 63-81 行 展示了先用light.toJSON()导出、再用ObjectLoader.parse()重建并断言阴影配置完全一致的用法。
组合实战:把参数拧到刚好的位置
以下示例综合展示了如何为一个场景配置一套"阴影质量与性能平衡"的参数(可对照 examples/webgl_shadowmap.html 中SHADOW_MAP_WIDTH/renderer.shadowMap的用法):
// —— 渲染器全局:软阴影算法,自适应更新 —— renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFShadowMap; // 或 VSMShadowMap // —— 平行光阴影:只覆盖需要的区域以提升贴图密度 —— const dirLight = new THREE.DirectionalLight( 0xffffff, 2 ); dirLight.castShadow = true; dirLight.shadow.mapSize.set( 2048, 2048 ); // 2 的幂 dirLight.shadow.camera.left = -20; dirLight.shadow.camera.right = 20; dirLight.shadow.camera.top = 20; dirLight.shadow.camera.bottom = -20; dirLight.shadow.camera.updateProjectionMatrix(); dirLight.shadow.normalBias = 0.05; // 优先用法线偏移缓解痤疮 dirLight.shadow.bias = 0.0001; // 极小的深度偏差兜底 // —— 聚光灯阴影:配合 focus 提升有效分辨率 —— const spotLight = new THREE.SpotLight( 0xffffff, 1, 0, Math.PI / 6 ); spotLight.castShadow = true; spotLight.shadow.mapSize.set( 1024, 1024 ); spotLight.shadow.focus = 0.7; // 视场角收缩为聚光锥的 70% spotLight.shadow.radius = 4; // PCF 下让软阴影边缘更宽(留意 banding) // —— 静态场景性能优化 —— spotLight.shadow.autoUpdate = false; // 灯与场景都不动时关闭自动更新 // 每次移动光源后手动刷新: spotLight.shadow.needsUpdate = true; renderer.render( scene, camera );排查阴影问题时,可按"由表及里"的顺序检查:
- 完全没阴影:确认
renderer.shadowMap.enabled、光源castShadow、接收面receiveShadow三者齐全; - 阴影痤疮/闪烁:先小幅上调
normalBias,不足再上调bias; - 阴影边缘锯齿明显:提高
mapSize或改用PCFShadowMap并调大radius; - 阴影"漏光"或物体浮空:
bias过大,往回调小; - VSM 阴影边缘糊、出现光斑:在支持范围内调大
blurSamples,同时注意 VSM 不支持点光源。
延伸阅读
- DirectionalLightShadow 文档 与 DirectionalLight 文档:平行光阴影相机各边界的含义与用法;
- SpotLightShadow 文档、PointLightShadow 文档:各自子类专属参数;
- 核心实现 src/lights/LightShadow.js 及其渲染期消费者 src/renderers/webgl/WebGLShadowMap.js、节点管线端 src/nodes/lighting/ShadowNode.js;
- 单元测试 test/unit/src/lights/DirectionalLightShadow.tests.js 与 test/unit/src/lights/SpotLightShadow.tests.js,验证继承、克隆与序列化行为;
- 可运行示例 examples/webgl_shadowmap.html。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考