three.js PointLightShadow 深度解析:点光源全方位阴影的配置、六面深度贴图与渲染原理
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
PointLightShadow是 three.js 中PointLight(点光源)专用的阴影配置对象,负责管理点光源在世界中全方向投射阴影所需的内部相机、深度贴图尺寸以及各类阴影采样参数。本文以 PointLightShadow 官方文档页 为骨架,结合当前仓库中 PointLightShadow.js、LightShadow.js、PointLight.js 与 WebGLShadowMap.js 等源码实现,完整讲解它的构造函数、属性体系、实际使用步骤与底层六面立方体贴图渲染原理。读完本文,你将掌握点光源阴影的完整配置方法、性能瓶颈的成因,以及在不同渲染器/阴影类型下的兼容性边界。
概述与类继承关系
官方文档原句:Represents the shadow configuration of point lights.(表示点光源的阴影配置。)
由于点光源从单个位置向四周 360° 发光,它的阴影也与方向光(DirectionalLightShadow)、聚光灯(SpotLightShadow)截然不同:必须记录以光源为球心、全方向各个方向上的深度信息,才能正确判断场景中任意位置是否处于点光源的阴影中。
继承链:LightShadow → PointLightShadow
从源码 PointLightShadow.js 可以看出它的继承关系:
import { LightShadow } from './LightShadow.js'; import { PerspectiveCamera } from '../cameras/PerspectiveCamera.js'; class PointLightShadow extends LightShadow { constructor() { super( new PerspectiveCamera( 90, 1, 0.5, 500 ) ); this.isPointLightShadow = true; } } export { PointLightShadow };也就是说:
LightShadow(src/lights/LightShadow.js)是所有光阴影的抽象基类,承载了bias、normalBias、mapSize、radius、map、autoUpdate等通用阴影属性以及矩阵更新、序列化等通用方法;PointLightShadow只做两件事:向基类传入一台透视相机作为点光源观察世界的视角,并标记自身的类型标志isPointLightShadow = true。
使用入口:Light 上的 shadow 属性
PointLightShadow不会由你手动实例化,而是在创建PointLight时自动挂载到其.shadow属性上。见 PointLight.js:
/** * This property holds the light's shadow configuration. * @type {PointLightShadow} */ this.shadow = new PointLightShadow();因此,日常使用时的访问链是pointLight.shadow,例如pointLight.shadow.mapSize.set( 512, 512 )。
构造函数:new PointLightShadow()
签名如下:
new PointLightShadow()构造函数不接受任何参数。在 PointLightShadow.js 中,构造过程的核心是调用基类构造函数并传入:
super( new PerspectiveCamera( 90, 1, 0.5, 500 ) );这行代码揭示了点光源阴影的本质:渲染器为点光源创建了一台专用的透视相机,参数含义如下:
| 参数 | 值 | 作用 |
|---|---|---|
fov | 90(度) | 视锥体垂直张角。配合立方体贴图的每个面(每面恰好覆盖 90°×90° 的立体角)使用 |
aspect | 1 | 纵横比 1:1,保证每个立方体面是正方形视口 |
near | 0.5 | 近裁剪面,小于该距离的物体不会被记录进深度 |
far | 500 | 远裁剪面,超出该距离的物体不产生阴影 |
值得注意的是,这个默认far = 500只在不设置PointLight.distance时生效——当PointLight实例设置了最大照明距离distance后,渲染器会用light.distance替换相机far(详见下文"渲染原理"一节),这与 PointLight 源码中distance = 0表示"无限远不衰减"的语义是对应的。
属性
.isPointLightShadow : boolean(只读)
这是PointLightShadow最重要的一个自有属性:
| 属性 | 值 | 说明 |
|---|---|---|
isPointLightShadow | true | 只读类型标志,用于类型测试 |
官方文档原句:This flag can be used for type testing.(该标志可用于类型测试。)在 PointLightShadow.js 中被赋值为true。
它并非摆设,而是被底层渲染路径反复使用的关键分支依据,例如:
- ShadowNode.js 中用
shadow.isPointLightShadow !== true判定是否走VSM(方差阴影贴图)采样分支; - WebGLShadowMap.js 中同样用它决定是否对阴影贴图执行 VSM 模糊 pass;
- ShadowNode.js 中决定阴影深度纹理取普通深度纹理还是 VSM 水平模糊纹理。
继承自 LightShadow 的核心属性
PointLightShadow的属性主要继承自基类 LightShadow.js,下列表格完整列出对实际效果影响最大的配置项(默认值均以当前仓库源码为准):
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
camera | PerspectiveCamera | 见上文 | 光源观察世界的相机,直接决定阴影记录范围 |
intensity | number | 1 | 阴影强度,取值区间[0, 1] |
bias | number | 0 | 深度偏移,微调量级约0.0001,可减轻阴影痤疮等伪影 |
biasNode | ?Node<float> | null | bias的节点版本,仅WebGPURenderer支持;一旦定义,bias失效 |
normalBias | number | 0 | 沿法线偏移采样位置,适合大场景浅入射角下的阴影痤疮,代价是阴影可能变形 |
radius | number | 1 | 大于 1 时模糊阴影边缘;类型为BasicShadowMap时无效 |
blurSamples | number | 8 | VSM 阴影贴图模糊的采样数 |
mapSize | Vector2 | (512, 512) | 阴影贴图宽高,需为 2 的幂,越大越清晰但越耗性能 |
mapType | number | UnsignedByteType | 阴影纹理的类型 |
map | ?RenderTarget | null | 渲染期间内部生成的深度图(立方体贴图渲染目标) |
mapPass | ?RenderTarget | null | VSM 路径下内部生成的分布图 |
matrix | Matrix4 | 新的Matrix4 | 模型到阴影相机空间矩阵,渲染期间内部计算 |
autoUpdate | boolean | true | 是否自动更新阴影 |
needsUpdate | boolean | false | 手动置true并调用一次render以强制更新 |
与方向光/聚光灯阴影最大的不同是:mapSize的语义是每个立方体面的尺寸(六个面共享),因此总纹理开销约等于 6 ×mapSize²(详见性能小节)。
点光源阴影的 far 联动:light.distance
点光源阴影相机默认near = 0.5、far = 500,但far会被PointLight的distance覆盖。逻辑在 WebGLShadowMap.js:
const far = light.distance || camera.far; if ( far !== camera.far ) { camera.far = far; camera.updateProjectionMatrix(); }即:light.distance非 0 时用distance作为阴影远裁剪面(保证只有灯光实际照射范围内的物体投影),distance = 0(无限远)时退回相机默认far = 500。这意味着如果希望无限远点光源的阴影覆盖更大范围,需要显式放大相机 far,例如:
light.shadow.camera.far = 2000; // 或在受支持时配合 renderer.shadowMap在场景中启用点光源阴影(完整实战示例)
与 three.js 所有阴影一致,点光源阴影需要三处配合才能生效:渲染器开关、光源与投影物体属性、接收物体属性。以官方示例 examples/webgl_shadowmap.html 的用法为参照,一个最小可运行示例为:
import * as THREE from './build/three.module.js'; const renderer = new THREE.WebGLRenderer(); renderer.shadowMap.enabled = true; // 1. 打开渲染器阴影总开关 renderer.shadowMap.type = THREE.PCFShadowMap; // 2. 点光源推荐 PCFShadowMap(见"兼容性边界") const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera( 60, innerWidth / innerHeight, 0.1, 100 ); // 地面:接收阴影 const ground = new THREE.Mesh( new THREE.PlaneGeometry( 20, 20 ), new THREE.MeshStandardMaterial( { color: 0x888888 } ) ); ground.rotation.x = - Math.PI / 2; ground.receiveShadow = true; scene.add( ground ); // 投影物体 const box = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), new THREE.MeshStandardMaterial( { color: 0xff0000 } ) ); box.castShadow = true; // 3. 物体投射阴影 scene.add( box ); // 点光源(灯丝/灯泡式全向光源) const light = new THREE.PointLight( 0xffffff, 1, 0, 2 ); // distance=0 无限远,decay=2 light.position.set( 2, 3, 2 ); light.castShadow = true; // 4. 光源投射阴影 light.shadow.mapSize.set( 512, 512 ); // 5. 每个立方体面 512×512 light.shadow.bias = 0.0001; // 轻微偏移抑制痤疮 scene.add( light );注意第四步中light.castShadow = true会在渲染帧内把该光源加入阴影渲染列表;随后renderer.shadowMap(其类型见 WebGLShadowMap.js 的this.enabled、this.autoUpdate、this.needsUpdate、this.type等开关)才开始工作。
渲染原理:点光源为什么需要六面深度贴图
这是点光源阴影区别于方向光、聚光灯的核心机制。官方文档的继承页只描述了"LightShadow →",而真正的六面渲染逻辑位于 WebGLShadowMap.js。
1. 创建立方体深度渲染目标
当检测到light.isPointLight时(WebGLShadowMap.js),渲染器创建的shadow.map不是普通 2D 渲染目标,而是WebGLCubeRenderTarget,并配套CubeDepthTexture:
shadow.map = new WebGLCubeRenderTarget( _shadowMapSize.x ); shadow.map.depthTexture = new CubeDepthTexture( _shadowMapSize.x, UnsignedIntType );一个立方体贴图共有 6 个面(+X/-X/+Y/-Y/+Z/-Z),分别记录点光源朝六个方向观察到的深度。
2. 逐个面摆好"影子相机"并渲染
渲染循环对每个光源先求出渲染面数(WebGLShadowMap.js):
const faceCount = shadow.map.isWebGLCubeRenderTarget ? 6 : shadow.getViewportCount();点光源命中"6 个面"的分支。随后在 WebGLShadowMap.js 中,每一帧对 6 个面分别执行:
- 把内部透视相机放到点光源的世界位置(
camera.position.copy( _lightPositionWorld )); - 通过预置的
_cubeDirections[ face ](六组单位方向)与_cubeUps[ face ](六组上向量)让相机lookAt朝向对应面; camera.updateMatrixWorld()后用投影矩阵 × 视图逆矩阵生成该面的投影矩阵与视锥体,用于物体剔除;renderer.setRenderTarget( shadow.map, face )把渲染目标切到立方体贴图的第face个面并clear()(WebGLShadowMap.js);- 用
MeshDistanceMaterial(内部距离材质,见下节)把所有castShadow的网格渲染进该面深度。
3. 帧率与性能的第一来源:6 倍开销
从上述流程可以推断:每帧点光源会把场景中的投射物渲染 6 次(6 个立方体面各一次)。这就是文档注释里mapSize"越高越好但越耗计算时间"在点光源上被放大 6 倍的原因。若同时存在最大纹理尺寸限制,渲染器会按shadowFrameExtents换算并钳制每个面的大小(WebGLShadowMap.js),不会无限放大。
兼容性边界:哪些阴影类型可用、哪些不可用
围绕isPointLightShadow这个标志,当前仓库对点光源阴影的类型支持存在两个明确的硬性边界(均有源码警告为证):
VSM(
VSMShadowMap)不支持点光源。WebGLShadowMap.js 会输出:WebGLShadowMap: VSM shadow maps are not supported for PointLights. Use PCF or BasicShadowMap instead.并跳过该光源。因为 VSM 的两次水平/垂直模糊 pass 是针对单一 2D 深度图设计的,与 6 面深度图结构冲突(WebGLShadowMap.js 中也明确以shadow.isPointLightShadow !== true作为执行 VSM pass 的前提)。PCFSoftShadowMap在当前仓库的 WebGL 路径中已回退为PCFShadowMap(WebGLShadowMap.js 输出PCFSoftShadowMap has been removed. Using PCFShadowMap instead.)。
因此,在 WebGL 渲染器下点光源阴影实际可用的是BasicShadowMap(硬阴影、无滤波)与PCFShadowMap(PCF 软阴影);radius > 1的边缘模糊只在 PCF 下有效。
而 TSL/WebGPU 节点路径中,点光源阴影走的是 PointShadowNode.js 的pointShadow( light, shadow )节点,同样会用到shadow.isPointLightShadow来判断是否采用立方体深度采样(见 ShadowNode.js),从而在底层代码中复用"点光源不适用 VSM 两趟模糊"的结论。
用 MeshDistanceMaterial 自定义点光源的投影材质
为了让点光源记录"到光源的距离"而非普通深度,渲染器内部使用 MeshDistanceMaterial(官方文档描述其为A material used internally for implementing shadow mapping with point lights)。在 WebGLShadowMap.js 中可以看到选取规则:
const customMaterial = ( light.isPointLight === true ) ? object.customDistanceMaterial : object.customDepthMaterial; result = ( light.isPointLight === true ) ? _distanceMaterial : _depthMaterial;这带来一个实用的扩展点:把某个网格的customDistanceMaterial替换成自定义的MeshDistanceMaterial实例,即可控制该网格在点光源阴影中如何投影。例如保留透明贴图细节、让透明区域不投影,正如 MeshDistanceMaterial.js 顶部注释所示(把实例赋给Object3D.customDistanceMaterial以保证物体透明部分不投影)。需要复制一份默认材质修改时,可在该物体的onBeforeShadow/onAfterShadow钩子中按需调整(钩子在 WebGLShadowMap.js 中被调用)。
序列化与资源释放
PointLightShadow本身只新增了类型标志,序列化工作由继承方法完成:
- 序列化:
PointLight的 toJSON 会将shadow.toJSON()写入data.object.shadow,而 LightShadow.js 的toJSON()输出intensity、bias、normalBias、radius、blurSamples、mapSize以及内部相机(剔除matrix)——这正是编辑器导出与ObjectLoader.parse重建点光源阴影配置的依据; - 拷贝:
LightShadow.copy(LightShadow.js)会克隆相机并逐项复制上述配置,PointLight的 copy 则通过this.shadow = source.shadow.clone()复制整个阴影对象; - 释放:PointLight.dispose 会调用
this.shadow.dispose(),进而释放 GPU 上的map/mapPass渲染目标(LightShadow.js)。在你的应用移除点光源时调用light.dispose()即可避免 GPU 资源泄漏。
点光源阴影调优速查
综合源码语义,实践中针对PointLightShadow的常用调优手段可归结为:
- 控制 mapSize 与数量:
mapSize每增大 1 倍,6 个面合计开销增大 4×6 倍;多盏点光源叠加时要格外克制,优先用light.shadow.mapSize.set( 256, 256 )起步; - 根据场景范围设置相机范围:对
distance = 0的无限远点光源,按需调大light.shadow.camera.far;对设置了distance的点光源则无需手动设置(渲染器自动联动); - 对抗伪影:出现阴影痤疮时把
light.shadow.bias从0.0001起逐步微调;大场景浅入射角痤疮优先考虑normalBias; - 选择正确的类型:点光源在 WebGL 渲染器下只使用
PCFShadowMap或BasicShadowMap;需要柔和边缘时用radius > 1(PCF),需要最高性能时用BasicShadowMap; - 静态场景省电:阴影静止时设置
light.shadow.autoUpdate = false,需要更新时再置needsUpdate = true并触发一次渲染(两个开关均继承自 LightShadow.js); - 透明物体的投影细节:通过给网格赋
customDistanceMaterial = new THREE.MeshDistanceMaterial(...)定制其点光源投影行为。
小结
PointLightShadow虽然代码量极少(仅 31 行的 PointLightShadow.js),却在 three.js 阴影体系中扮演了承上启下的角色:它通过向LightShadow基类注入PerspectiveCamera( 90, 1, 0.5, 500 )与只读标志isPointLightShadow,把"点光源 = 全方向光源"的几何事实转化为"每帧渲染 6 面立方体深度图"的渲染管线,并依托isPointLightShadow标志在各渲染路径中正确禁用不兼容的 VSM、启用立方体深度采样。理解了它的构造与这一套底层分支逻辑,你就能精准地预测并控制点光源阴影的画质与性能开销。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考