首页
/ three.js PointShadowNode 源码全解析:WebGPU 点光源立方体贴图阴影的实现原理与使用

three.js PointShadowNode 源码全解析:WebGPU 点光源立方体贴图阴影的实现原理与使用

2026-09-07 09:32:42作者:秋阔奎Evelyn

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 的公开属性(如 mapSizebiasradius)即可,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.htmlwebgl_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(反向深度缓冲)在 GreaterEqualCompareLessEqualCompare 之间切换,保证阴影深度比较与主深度缓冲约定一致;
  • 方法返回 { shadowMap, depthTexture } 结构,被父类 setupShadow() 解构后挂载为 this.shadowMap,并把 shadowMap 写回 shadow.mapShadowNode 源码)。

renderShadow:六次渲染写满立方体六个面

renderShadow( frame ) 覆写了父类"一次渲染一张 shadow map"的默认逻辑,改为循环 6 次、逐面渲染点光源的阴影深度(源码)。其流程为:

  1. 备份渲染器的 autoClear、clearColor、clearAlpha,随后临时关闭 autoClear 并以 shadow.clearColor/clearAlpha 清屏,避免六面相互污染;
  2. 对每个面 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 )
  3. 最后恢复 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)并把结果与过滤值做 selectShadowNode 源码);点光源版则直接交给 TSL 函数 pointShadowFilter第 97-142 行),内部逻辑为:

  1. shadowCoord(光源到片元方向)的三轴绝对值最大值 viewZ,得到该片元到光源的距离,用于判断它是否落在 [shadow.camera.near, shadow.camera.far] 区间内——区间外的片元直接得到 float(1.0)(完全无阴影),跳过昂贵的过滤采样;
  2. 在区间内,把 viewZ 依渲染器深度缓冲模式换算为用于立方体贴图比较的深度值 dp
    • reversedDepthBuffer:使用反向透视深度并 - bias
    • logarithmicDepthBuffer:使用对数深度并 + bias
    • 默认:使用透视深度并 + bias
  3. 归一化光源→片元方向得到 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":

  1. 从 shadow 对象读取 radius(过滤半径)与 mapSize,计算 texelSize = radius / mapSize.x
  2. 围绕采样方向 bd3D 构造一个切空间坐标系:用 bd3D 与坐标轴叉积得到 tangentbitangent,把二维偏移"立起来"成为三维锥形扰动;
  3. 引入 interleavedGradientNoise( screenCoordinate.xy )(IGN 交错渐变噪声)产生逐像素旋转角 φ = noise × 2π打破采样模式规律、抑制条纹状走样
  4. 通过 vogelDiskSample( i, 5, phi ) 在切空间内均匀铺开 5 个 Vogel 圆盘样本,分别沿 bd3D + (tangent·x + bitangent·y)·texelSize 做立方体贴图深度比较;
  5. 五次比较结果累加后 × 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 本身只承载渲染逻辑,所有可调参数都来自它引用的 PointLightShadowLightShadow。下表整理了关键属性的源码默认值(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 区间检查中会直接把远处片元判为无阴影。

相关资源索引

小结

PointShadowNode 是 three.js 节点化渲染管线为点光源阴影量身定制的实现:它用立方体渲染目标 + 六面顺序渲染覆盖 360° 全向视角,用"光源→片元方向"替代传统 UV 坐标完成采样,并提供 Basic 单点比较与 Vogel/IGN 5 样本 PCF 两档过滤质量。当你在 WebGPU 渲染器下设置点光源并开启 renderer.shadowMap 时,上述全部机制会经由 PointLightNode 自动接线。理解它的构造签名、四个覆写方法与两个过滤函数,是自定义点光源阴影效果、排查阴影异常或深度定制光照节点的起点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395