three.js PointShadowNode 源码全解析:WebGPU 点光源立方体贴图阴影的实现原理与使用
PointShadowNode 是 three.js 基于 Node/TSL 的着色器系统中点光源(PointLight)阴影的核心实现节点,负责将点光源六面深度信息渲染进立方体贴图,并采样生成带软阴影过滤的阴影结果。本文将以该节点的官方 API 文档为主线,结合 PointShadowNode 源码 及其父类 ShadowNode 的实现细节,逐层拆解它的继承体系、构造方式、六大生命周期方法与两类点光源专属阴影过滤函数,帮助你彻底掌握 three.js WebGPU 渲染器下点光源阴影的完整工作链路。
PointShadowNode 的定位与继承体系
PointShadowNode 表示点光源节点的阴影实现("Represents the shadow implementation for point light nodes"),属于 three.js Node 材质/WebGPU 渲染体系中"光照节点(Lighting Node)"的一部分。
其完整继承链为:
EventDispatcher → Node → ShadowBaseNode → ShadowNode → PointShadowNode
各层职责从源码中可清晰读出:
Node提供节点图与求值的基础能力;- ShadowBaseNode 是所有阴影节点的公共基类,它持有关键的
light引用,通过 setupShadowPosition 绑定世界空间顶点位置shadowPositionWorld,并复用一份缓存的 ShadowMaterial(_getShadowMaterial)来渲染阴影对象; - ShadowNode 是默认(平行光、聚光灯等)阴影的通用实现,定义了
setup()、setupShadow()、setupShadowFilter()、setupShadowCoord()、renderShadow()、updateBefore()等完整的阴影节点骨架; PointShadowNode继承 ShadowNode 并针对点光源的特殊性(360° 全向投影、六面渲染、立方体深度纹理)逐项覆写上述方法。
一个直观的触发入口是 PointLightNode.setupShadowNode:当点光源被接入节点着色管线时,它直接调用
setupShadowNode() {
return pointShadow( this.light );
}
这里的 pointShadow 正是 PointShadowNode.js 文件底部导出的 TSL 工厂函数,等价于 new PointShadowNode( light, shadow )(源码)。也就是说:只要你的渲染场景中存在投阴影的点光源,WebGPU/节点渲染管线内部就会自动实例化一个 PointShadowNode 来驱动它的阴影。
构造函数与 light / shadow 两个关键参数
官方 API 定义的构造签名如下:
new PointShadowNode( light : PointLight, shadow : PointLightShadow )
| 参数 | 类型 | 说明 |
|---|---|---|
light |
PointLight | 产生阴影的点光源(必需) |
shadow |
PointLightShadow | 可选的阴影配置对象,默认值为 null |
实际构造逻辑非常简单(源码),只是把两个参数转发给父类:
constructor( light, shadow = null ) {
super( light, shadow );
}
关键行为发生在父类 ShadowNode 构造函数 中:
this.shadow = shadow || light.shadow——不传shadow时自动回退到光源自身的阴影对象。普通PointLight默认携带一个 PointLightShadow,后者内部固定使用一张PerspectiveCamera( 90, 1, 0.5, 500 )作为立方体六个面的投影相机;this.shadowMap = null初始为空,首次渲染时由setupShadow()通过setupRenderTarget()创建;- 节点被标记为
isShadowNode = true,供引擎做类型判断。
这意味着日常使用时基本只需操作 PointLight.shadow 的公开属性(如 mapSize、bias、radius)即可,PointShadowNode 的构造由引擎自动完成:
import * as THREE from 'three';
import WebGPURenderer from 'three/webgpu';
const light = new THREE.PointLight( 0xffffff, 50, 30, 2 );
light.castShadow = true;
light.shadow.mapSize.set( 1024, 1024 );
light.shadow.bias = 0.0001;
light.shadow.radius = 4; // 仅 PCF 软阴影生效
const renderer = new WebGPURenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap; // 或 BasicShadowMap
可对照仓库示例 webgpu_shadowmap_pointlight.html 与 webgl_shadowmap_pointlight.html 查看完整的场景搭建与效果对比。
逐方法解析:PointShadowNode 覆写的四大核心方法
文档中共列出四个覆写方法(外加一个返回过滤函数的 getShadowFilterFn)。它们分别对应点光源阴影从"渲染目标创建 → 六面场景渲染 → 坐标采样 → 过滤输出"的完整链路。
setupRenderTarget:创建 CubeRenderTarget + CubeDepthTexture
setupRenderTarget( shadow, builder ) {
const depthTexture = new CubeDepthTexture( shadow.mapSize.width );
depthTexture.name = 'PointShadowDepthTexture';
depthTexture.compareFunction = builder.renderer.reversedDepthBuffer ? GreaterEqualCompare : LessEqualCompare;
const shadowMap = builder.createCubeRenderTarget( shadow.mapSize.width );
shadowMap.texture.name = 'PointShadowMap';
shadowMap.depthTexture = depthTexture;
return { shadowMap, depthTexture };
}
与 ShadowNode.setupRenderTarget 创建普通 2D DepthTexture + RenderTarget 不同,点光源必须覆盖 360° 全向视角,因此这里调用 builder.createCubeRenderTarget() 生成立方体渲染目标,并为它配套一个同名尺寸的 CubeDepthTexture 作为深度附件。
实现要点:
mapSize.width同时作为宽、高使用(立方体六个面都是正方形);- 深度纹理的
compareFunction会根据渲染器是否开启reversedDepthBuffer(反向深度缓冲)在GreaterEqualCompare与LessEqualCompare之间切换,保证阴影深度比较与主深度缓冲约定一致; - 方法返回
{ shadowMap, depthTexture }结构,被父类setupShadow()解构后挂载为this.shadowMap,并把shadowMap写回shadow.map(ShadowNode 源码)。
renderShadow:六次渲染写满立方体六个面
renderShadow( frame ) 覆写了父类"一次渲染一张 shadow map"的默认逻辑,改为循环 6 次、逐面渲染点光源的阴影深度(源码)。其流程为:
- 备份渲染器的
autoClear、clearColor、clearAlpha,随后临时关闭autoClear并以shadow.clearColor/clearAlpha清屏,避免六面相互污染; - 对每个面
face = 0..5:renderer.setRenderTarget( shadowMap, face )把渲染目标切换到立方体贴图的对应面并clear();- 取
far = light.distance || camera.far,若与相机far不一致则更新投影矩阵——保证阴影有效距离与光源实际照明距离一致; - 将相机平移到光源世界位置
light.matrixWorld,再通过预定义的朝向表让相机对准当前立方体面,并camera.updateMatrixWorld(); - 构造阴影平移矩阵,重建平截头体用于视锥剔除,然后以当前场景名打上
Point Light Shadow [ lightName ] - Face N标记调用renderer.render( scene, camera );
- 最后恢复
autoClear与清屏参数。
其中最值得注意的工程细节是坐标系差异:源码中为 WebGPU 与 WebGL 各准备了一套立方体面方向/上向量表(第 25-44 行):
_cubeDirectionsWebGL遵循 OpenGL 惯例,+Y 面的 up 为 +Z;_cubeDirectionsWebGPU为匹配 WebGPU 纹理采样约定,把 ±Y 两个面的方向与 up 做了对调(+Y 方向(0,-1,0)朝上改用(0,0,-1),-Y 方向(0,1,0)的 up 为(0,0,1))。
是否切换取决于 renderer.coordinateSystem === WebGPUCoordinateSystem(第 250-252 行)。如果自定义覆盖此方法,必须同样区分两套坐标系,否则阴影面会镜像错乱。
setupShadowCoord:原样保留未修正的 shadow position
setupShadowCoord( builder, shadowPosition ) {
return shadowPosition;
}
父类 ShadowNode.setupShadowCoord 会把齐次裁剪坐标做透视除法(除以 w)、翻转 Y 并施加 bias,得到最终用于 2D 阴影贴图采样的 (u, v) 坐标。
而点光源的阴影采样方向是以光源为球心的空间方向而非 UV,因此覆写后的实现直接返回未做任何换算的 shadowPosition。该值会被 pointShadowFilter 复用——源码注释说明得很清楚:"for point lights, the uniform @vShadowCoord is re-purposed to hold the vector from the light to the world-space position of the fragment."(shadowCoord 在此被"重新利用"为光源指向片元的向量)。
setupShadowFilter:点光源专属过滤入口
setupShadowFilter( builder, { filterFn, depthTexture, shadowCoord, shadow } ) {
return pointShadowFilter( { filterFn, depthTexture, shadowCoord, shadow } );
}
父类实现中会做 0~1 的视锥裁剪测试(frustumTest)并把结果与过滤值做 select(ShadowNode 源码);点光源版则直接交给 TSL 函数 pointShadowFilter(第 97-142 行),内部逻辑为:
- 取
shadowCoord(光源到片元方向)的三轴绝对值最大值viewZ,得到该片元到光源的距离,用于判断它是否落在[shadow.camera.near, shadow.camera.far]区间内——区间外的片元直接得到float(1.0)(完全无阴影),跳过昂贵的过滤采样; - 在区间内,把
viewZ依渲染器深度缓冲模式换算为用于立方体贴图比较的深度值dp:reversedDepthBuffer:使用反向透视深度并- bias;logarithmicDepthBuffer:使用对数深度并+ bias;- 默认:使用透视深度并
+ bias;
- 归一化光源→片元方向得到
bd3D(base direction 3D),调用filterFn完成实际采样过滤。
两种点光源阴影过滤函数:Basic 与 PCF 软阴影
与文档描述一致,getShadowFilterFn( type ) 覆写父类基于 _shadowFilterLib 数组(ShadowNode 源码)的分发逻辑,改为只返回点光源专属的过滤函数:
getShadowFilterFn( type ) {
return type === BasicShadowMap ? BasicPointShadowFilter : PointShadowFilter;
}
BasicPointShadowFilter:单次深度比较
export const BasicPointShadowFilter = Fn( ( { depthTexture, bd3D, dp } ) => {
return cubeTexture( depthTexture, bd3D ).compare( dp );
} );
对应 renderer.shadowMap.type = THREE.BasicShadowMap。它直接用硬件纹理比较(cubeTexture(...).compare(dp))沿 bd3D 方向采样立方体深度图做单点判断,开销最低,但边缘锯齿明显。
PointShadowFilter:Vogel 圆盘 + IGN 旋转的 5 样本 PCF
对应 THREE.PCFShadowMap(默认),是点光源软阴影的 PCF 近似(源码第 66-95 行)。算法思路与二维 PCF 完全不同,因为点光源阴影深度是"距离"而非"平面 UV":
- 从 shadow 对象读取
radius(过滤半径)与mapSize,计算texelSize = radius / mapSize.x; - 围绕采样方向
bd3D构造一个切空间坐标系:用bd3D与坐标轴叉积得到tangent、bitangent,把二维偏移"立起来"成为三维锥形扰动; - 引入
interleavedGradientNoise( screenCoordinate.xy )(IGN 交错渐变噪声)产生逐像素旋转角φ = noise × 2π,打破采样模式规律、抑制条纹状走样; - 通过
vogelDiskSample( i, 5, phi )在切空间内均匀铺开 5 个 Vogel 圆盘样本,分别沿bd3D + (tangent·x + bitangent·y)·texelSize做立方体贴图深度比较; - 五次比较结果累加后
× 1/5取平均,得到柔和的半影过渡。
也就是说:radius 越大、采样锥角越宽,阴影半影越柔和。这也是为什么上面的示例中 PCF 模式下设置 light.shadow.radius 有实际意义。需注意,软阴影五个样本是固定 5 采样的轻量方案,换取点光源下可控的性能。
VSM 等类型的兼容性说明
父类 ShadowNode 的 setupShadow() 中,VSM 相关分支带有 shadow.isPointLightShadow !== true 的显式排除条件(ShadowNode 源码),updateShadow() 里的 VSM 模糊 pass 同样如此(源码)。结合 PointShadowNode.getShadowFilterFn 仅识别 BasicShadowMap 可以看出:点光源阴影目前只支持 BasicShadowMap 与 PCFShadowMap 两类,VSM(方差阴影图)等其余类型不会被 PointShadowNode 处理。
PointShadowNode 在一帧中的执行位置
理解上面的方法后,把它们挂回渲染循环就能看到完整时间线。PointShadowNode 本身不写 updateBefore,而是沿用 ShadowNode 的:
- ShadowNode.setup:编译阶段先检查
renderer.shadowMap.enabled,若开启则构建阴影输出节点(内部调用setupShadow→ 上述被覆写的三个 setup 方法),并把最终shadowOutput挂上 inspector 便于调试; - ShadowNode.updateBefore:每帧渲染前依据
shadow.autoUpdate / needsUpdate判断是否重绘阴影,并通过_cameraFrameId保证同一相机同一帧只更新一次; updateShadow()(源码)内部设置统一的 ShadowMaterial、调用被 PointShadowNode 覆写的renderShadow()完成六面渲染。
因此在 WebGPU 渲染器里,PointShadowNode 的典型一帧调用链可概括为:
renderer.render()
└─ NodeFrame → PointShadowNode.updateBefore() // 判帧/脏检查
└─ ShadowNode.updateShadow() // 挂 ShadowMaterial
└─ PointShadowNode.renderShadow() // 6 次:setRenderTarget(face) + render
└─ 主场景片元着色
└─ setupShadow() → setupShadowFilter()
└─ pointShadowFilter → PointShadowFilter / BasicPointShadowFilter
└─ cubeTexture(depthTexture).compare(dp) × 1/5
常用阴影参数与默认值速查
PointShadowNode 本身只承载渲染逻辑,所有可调参数都来自它引用的 PointLightShadow 与 LightShadow。下表整理了关键属性的源码默认值(LightShadow 构造函数):
| 属性 | 默认值 | 作用与适用说明 |
|---|---|---|
shadow.mapSize |
Vector2(512, 512) |
立方体每面深度图尺寸(实际按宽取方形)。1024/2048 可明显提升阴影清晰度但六面内存开销大 |
shadow.bias |
0 |
深度偏移,缓解阴影"表面波纹/痤疮"。点光源下过大易产生漏光(peter-panning) |
shadow.normalBias |
0 |
沿法线方向的偏移,配合 shadowPositionWorld 使用 |
shadow.radius |
1 |
仅 PCF(PointShadowFilter)生效,控制软阴影采样锥半径/半影宽度 |
shadow.intensity |
1 |
阴影强度,在 mix(1, shadowNode, intensity) 中调节明暗 |
shadow.autoUpdate |
true |
每帧自动重绘阴影地图 |
shadow.needsUpdate |
false |
手动标记强制重绘一次 |
| 相机近/远裁剪 | 0.5 / 500 |
PointLightShadow 内置透视相机的 near/far(会进一步被 light.distance 覆盖为 far) |
当渲染距离较远或光源 distance 较大时,请同步放大 shadow.camera.far(或依靠 light.distance),否则在 viewZ 区间检查中会直接把远处片元判为无阴影。
相关资源索引
- 官方 API 文档:docs/pages/PointShadowNode.html.md(另有编译后的 PointShadowNode.html)
- 核心实现:src/nodes/lighting/PointShadowNode.js
- 父类实现:src/nodes/lighting/ShadowNode.js、src/nodes/lighting/ShadowBaseNode.js
- 光照节点入口:src/nodes/lighting/PointLightNode.js、src/nodes/TSL.js
- 阴影参数对象:src/lights/PointLightShadow.js、src/lights/LightShadow.js
- 可运行示例:examples/webgpu_shadowmap_pointlight.html、examples/webgl_shadowmap_pointlight.html
小结
PointShadowNode 是 three.js 节点化渲染管线为点光源阴影量身定制的实现:它用立方体渲染目标 + 六面顺序渲染覆盖 360° 全向视角,用"光源→片元方向"替代传统 UV 坐标完成采样,并提供 Basic 单点比较与 Vogel/IGN 5 样本 PCF 两档过滤质量。当你在 WebGPU 渲染器下设置点光源并开启 renderer.shadowMap 时,上述全部机制会经由 PointLightNode 自动接线。理解它的构造签名、四个覆写方法与两个过滤函数,是自定义点光源阴影效果、排查阴影异常或深度定制光照节点的起点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00