深入解析 three.js LightProbeNode:用球形谐波光照探针实现节点式间接漫反射照明
导读
本文聚焦 three.js 节点材质(Node / TSL)体系中的 LightProbeNode,它是将「光照探针(Light Probe)」编码的球谐光照信息接入渲染管线、为物体提供间接漫反射(irradiance)照明的关键节点。阅读本文你将掌握:LightProbeNode 在类层级中的位置与作用、构造函数与 .lightProbe 属性的真实含义、update() 每帧上传球谐系数与强度缩放的内在机制,以及其 setup() 如何配合 getShIrradianceAt 将辐照度累加进光照上下文,进而被 PBR 等光照模型消费。
LightProbeNode 概述:它在节点化光照管线中扮演什么角色
在 three.js 的 WebGPU / TSL 节点管线中,所有光照参与计算的方式都从「固定 shader chunk」迁移为「Node 图」。LightProbeNode 正是负责把 LightProbe(光照探针) 这种特殊光源翻译成节点图结构的实现类。其模块定位在源注释中写得很直白:
Module for representing light probes as nodes.
它继承自 AnalyticLightNode,属于「解析式光源节点」家族,但其处理路径与方向光、点光、聚光完全不同——光照探针不沿某个方向发射光,而是用球谐函数系数编码空间中某一点周围的环境辐照度。
探针背后的物理模型:LightProbe 与 SphericalHarmonics3
要理解 LightProbeNode,先要理解它包装的数据来源 LightProbe:
LightProbe继承自Light,构造签名是LightProbe( sh = new SphericalHarmonics3(), intensity = 1 ),其中sh保存球形谐波编码的照明信息;- 它不发射光,而是「存储光线穿过 3D 空间的信息」,渲染时用它近似打到物体上的光照;
- three.js 目前实现的是漫反射光照探针(diffuse light probe),功能上等价于一张辐照度环境贴图(irradiance environment map);
- 探针的球谐数据通常由 LightProbeGenerator 从立方体贴图 / CubeRenderTarget 生成(如
LightProbeGenerator.fromCubeTexture、fromCubeRenderTarget),也可由 WebXR 等外部估计数据提供,用于增强现实中对真实环境光照的响应。
从源码结构看,LightProbeNode 会用到 sh.coefficients(9 组球谐系数)以及 light.intensity 强度值。节点管线中两者的自动绑定见下文。
类继承链
文档头标注的继承关系为:
EventDispatcher → Node → LightingNode → AnalyticLightNode → LightProbeNode
对照源码:LightProbeNode 直接继承 AnalyticLightNode,而 AnalyticLightNode 又继承 LightingNode(LightingNode 内部以 'vec3' 为输出类型并标记 isLightingNode = true),LightingNode 再继承核心 Node。换言之,LightProbeNode 天然具备节点的构造/序列化/更新机制,同时继承了 AnalyticLightNode 提供的每帧更新(updateType = NodeUpdateType.FRAME)、光源引用、颜色节点与阴影支持等基础设施。
需要强调的是:像大多数灯光节点一样,用户通常不会手动 new LightProbeNode( light )。当使用 WebGPU / Node 渲染器并把一个 LightProbe 加入场景时,节点库会自动完成映射。证据位于:
- StandardNodeLibrary.js:
this.addLight( LightProbeNode, LightProbe ); - BasicNodeLibrary.js:同样注册了
LightProbeNode → LightProbe。
因此 LightProbeNode 是 WebGPU 渲染器处理 LightProbe 光源时的默认节点实现。
构造函数:new LightProbeNode( light )
API 签名如下:
new LightProbeNode( light : LightProbe )
| 参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
light |
LightProbe |
null |
被表示的光照探针对象 |
对照 源码实现:
constructor( light = null ) {
super( light );
const array = [];
for ( let i = 0; i < 9; i ++ ) array.push( new Vector3() );
/**
* Light probe represented as a uniform of spherical harmonics.
*/
this.lightProbe = uniformArray( array );
}
构造函数做了两件事:
- 调用父类构造,把
light引用交给AnalyticLightNode(父类会同时初始化color、colorNode等字段,若光源声明了阴影还会注册 dispose 监听); - 预分配 9 个
Vector3并包装成UniformArrayNode。9 对应球谐的阶数展开:三阶球谐(band 0~2)共有1 + 3 + 5 = 9组系数,每组系数是场景中方向的 RGB 辐照度贡献,因此在 three.js 中用 9 个Vector3存储。
为什么用 UniformArrayNode
lightProbe 的类型是 UniformArrayNode——它不是运行期参与图形计算的普通表达式节点,而是上传到 GPU 的 uniform 数组。这意味着球谐系数在每个渲染帧作为常量数组供着色器读取,而节点图本身(辐照度求值逻辑)只需被构建一次,这符合父类 AnalyticLightNode 设定 updateType = NodeUpdateType.FRAME(每帧更新而非每渲染更新)的整体策略。
属性详解
.lightProbe : UniformArrayNode
文档中该属性的正式描述为:
Light probe represented as a uniform of spherical harmonics.
即以球谐 uniform 数组形式存在的光照探针。它保存的是可被 GPU 读取的 9 个 Vector3(lightProbe.array),下标 0..8 分别对应球谐 band 0(1 个直流项)、band 1(3 个线性项)与 band 2(5 个二次项)的系数。着色器侧正是通过 this.lightProbe.element( i ) 逐项访问这些系数的。
除自有属性外,LightProbeNode 还从 AnalyticLightNode 继承了一组在调试 / 二次开发中常用的字段:light(光源引用)、color(Color)、colorNode(颜色节点)、shadowNode、isAnalyticLightNode 等。对 LightProbe 而言阴影字段实际不会启用,但继承体系保持一致。
每帧数据同步:update( frame )
方法签名:
update( frame : NodeFrame ) : void
frame:当前节点帧(NodeFrame)引用;- 该方法**覆写(Overrides)**了
AnalyticLightNode#update。
父类默认实现只做一件事:this.color.copy( light.color ).multiplyScalar( light.intensity )。LightProbeNode 的覆写在此基础上追加了探针特有逻辑,源码:
update( frame ) {
const { light } = this;
super.update( frame );
for ( let i = 0; i < 9; i ++ ) {
this.lightProbe.array[ i ].copy( light.sh.coefficients[ i ] ).multiplyScalar( light.intensity );
}
}
这一段揭示了探针亮度的真实管线:
- 先调用父类
super.update(frame)更新基础颜色; - 循环拷贝 9 组球谐系数
light.sh.coefficients[i]到lightProbe.array[i]; - 每组系数都乘以
light.intensity。也就是说,LightProbe 的intensity是整体作用于球谐幅值的标量——调大探针强度等价于按比例放大每一阶球谐系数,从而整体提高该点的间接辐照度。
因为 light.sh 只有在运行时(例如异步生成立方体贴图、WebXR 实时估计)才可能变化,所以这种「每帧把 CPU 端系数同步进 GPU uniform」的写法,能够保证用户随时替换 probe.sh 或修改 probe.intensity,节点无需重建即自动反映最新值。这一设计使 LightProbe 能响应动态环境(如 WebXR 的真实光照估计数据)。
接入光照计算:setup( builder ) 与球谐辐照度函数
update 负责数据搬运,真正的光照数学在 setup 中完成。源码:
setup( builder ) {
const irradiance = getShIrradianceAt( normalWorld, this.lightProbe );
builder.context.irradiance.addAssign( irradiance );
}
拆解其语义:
normalWorld:世界空间法线访问器,是评估某点辐照度所需的方向输入;getShIrradianceAt( normalWorld, this.lightProbe ):把 9 组球谐系数与法线方向求值,输出一个 vec3 辐照度颜色;builder.context.irradiance.addAssign( irradiance ):把该探针贡献的辐照度累加进光照上下文中的irradiance累积器。
注意 LightProbeNode 覆写了父类 AnalyticLightNode#setup 中走 setupDirect/阴影分支的路径——它不走直接光,而是作为间接漫反射光源注入上下文。这正是文档开篇所述该类专有的职责边界。
getShIrradianceAt 的数学实现
辐照度求值函数位于 getShIrradianceAt.js,是 TSL 风格的纯函数节点,其核心是三阶球谐重建:
- band 0(直流):
coef[0] * 0.886227; - band 1(线性,对 y / z / x):
coef[1..3] * 2.0 * 0.511664; - band 2(二次项):
coef[4..7] * 2.0 * 0.429043以及coef[6]的z²组合项coef[6] * (z*z*0.743125 - 0.247708)、coef[8] * 0.429043 * (x² - y²)。
这些常数(0.886227、0.511664、0.429043 等)是球谐归一化常量,用于把 SH 系数转换回可叠加的辐照度颜色。整个 TSL 求值与经典 WebGL 着色管线中的 shGetIrradianceAt( worldNormal, lightProbe )(见 lights_pars_begin.glsl.js)在数学上对应,只是从固定 shader chunk 平移进了节点图。
context.irradiance 从哪来、被谁消费
builder.context.irradiance 由光照上下文节点初始化。LightingContextNode.js 中以 vec3().toVar( 'irradiance' ) 声明该累积变量。多个「间接光源节点」都会向其累加,例如:
AmbientLightNode(环境光):context.irradiance.addAssign( this.colorNode ),见 AmbientLightNode.js;HemisphereLightNode(半球光):按法线混合天空 / 地面颜色后累加,见 HemisphereLightNode.js;IrradianceNode:对任意辐照度节点值做累加,见 IrradianceNode.js;LightProbeNode:累加球谐求值结果,见 LightProbeNode.js。
最终该累积量被各光照模型的 indirectDiffuse 间接漫反射方法消费——例如物理(PBR)光照模型中,const { irradiance, reflectedLight } = builder.context 之后用 irradiance * BRDF_Lambert(...) 叠加间接漫反射贡献(见 PhysicalLightingModel.js);Phong、Toon 等光照模型也有同样的间接辐照度路径。换言之,把一个 LightProbe 放进 WebGPU 渲染器的场景,其效果本质上等价于不断向像素的间接漫反射项注入球谐辐照度,而 LightProbeNode 就是这段逻辑的编排者。
实际使用方式与代码示例
日常开发中并不需要直接接触 LightProbeNode 类,你只需要使用高层 API:创建 LightProbe 并加入场景,WebGPU 节点渲染器即自动构建对应的 LightProbeNode。流程如下:
import * as THREE from 'three';
import { LightProbeGenerator } from './examples/jsm/lights/LightProbeGenerator.js';
// 1. 先有环境辐照来源:渲染一张立方体贴图
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256 );
cubeRenderTarget.texture.type = THREE.HalfFloatType;
const cubeCamera = new THREE.CubeCamera( 1, 100, cubeRenderTarget );
scene.add( cubeCamera );
// 2. 用 LightProbeGenerator 从立方体贴图生成球谐系数(LightProbe 对象)
// 该函数会编码 scene 中放置的 envMap/环境内容
const probe = await LightProbeGenerator.fromCubeRenderTarget( renderer, cubeRenderTarget );
// 3. 调节强度(将整体放大球谐系数,从而增强间接漫反射)
probe.intensity = 0.8;
// 4. 将探针加入场景 —— WebGPU 渲染器会自动为它创建 LightProbeNode
scene.add( probe );
配合 WebGPURenderer(或设置 renderer.isWebGPUBackend 的 TSL 管线)时,节点库会自动以 new LightProbeNode(probe) 参与光照求值。可参考 examples 目录下 webgpu_lightprobe*.html 系列示例进行对照验证(如 examples/webgpu_lightprobe.html、examples/webgpu_lightprobes.html)。
调试与常见问题
- 看不到探针效果? 确认使用的是节点化渲染后端(WebGPU 或
three.webgpu.js)。经典 WebGL 渲染器走的是 shader chunk 管线而非 LightProbeNode。 - 想实时更新环境? LightProbeNode 每帧在
update中从light.sh.coefficients拷贝数据,因此直接改写probe.sh(例如 WebXR 光估计回调中替换 SH 数据)即可生效,无需重建材质或节点。 - 亮度差异如何调? 探针
intensity会以multiplyScalar形式作用于全部 9 组系数(LightProbeNode.js),调大即可整体增强辐照度;也可同时结合场景中的环境贴图间接光做对比。 - 与普通光源叠加方式不同。 LightProbeNode 只贡献
context.irradiance(间接漫反射累积器),不会走setupDirect的直接光路径,也不会产生阴影,这与点光/聚光节点的行为存在本质差异,排查光照不匹配时需要留意。
小结
LightProbeNode 是 three.js 节点光照体系中把「球谐光照探针」接入间接漫反射计算的唯一入口节点。它以 9 个 Vector3 的 UniformArrayNode 承载三阶球谐系数,在 update() 中每帧将 light.sh.coefficients 与 intensity 同步到 GPU uniform,并在 setup() 中借助 getShIrradianceAt 依据世界法线重建辐照度,累加进 context.irradiance,最终由 PBR / Phong / Toon 等光照模型的 indirectDiffuse 路径使用。理解这条「LightProbe → LightProbeNode → context.irradiance → 光照模型」的链路,是掌握 three.js WebGPU 光照架构以及调试真实环境光(含 WebXR 光估计)的重要基础。
延伸阅读
- API 参考页:LightProbeNode.html
- 节点源码:LightProbeNode.js
- 数据来源:LightProbe 与 LightProbeGenerator
- 球谐求值函数:getShIrradianceAt.js
- 父类实现:AnalyticLightNode / LightingNode
- 上下文初始化与消费:LightingContextNode.js、PhysicalLightingModel.js
- 节点库自动映射:StandardNodeLibrary.js
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 StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00