首页
/ 深入解析 three.js LightProbeNode:用球形谐波光照探针实现节点式间接漫反射照明

深入解析 three.js LightProbeNode:用球形谐波光照探针实现节点式间接漫反射照明

2026-09-07 18:17:35作者:瞿蔚英Wynne

导读

本文聚焦 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.fromCubeTexturefromCubeRenderTarget),也可由 WebXR 等外部估计数据提供,用于增强现实中对真实环境光照的响应。

从源码结构看,LightProbeNode 会用到 sh.coefficients(9 组球谐系数)以及 light.intensity 强度值。节点管线中两者的自动绑定见下文。

类继承链

文档头标注的继承关系为:

EventDispatcher → Node → LightingNode → AnalyticLightNode → LightProbeNode

对照源码:LightProbeNode 直接继承 AnalyticLightNode,而 AnalyticLightNode 又继承 LightingNodeLightingNode 内部以 'vec3' 为输出类型并标记 isLightingNode = true),LightingNode 再继承核心 Node。换言之,LightProbeNode 天然具备节点的构造/序列化/更新机制,同时继承了 AnalyticLightNode 提供的每帧更新(updateType = NodeUpdateType.FRAME)、光源引用、颜色节点与阴影支持等基础设施。

需要强调的是:像大多数灯光节点一样,用户通常不会手动 new LightProbeNode( light )。当使用 WebGPU / Node 渲染器并把一个 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 );

}

构造函数做了两件事:

  1. 调用父类构造,把 light 引用交给 AnalyticLightNode(父类会同时初始化 colorcolorNode 等字段,若光源声明了阴影还会注册 dispose 监听);
  2. 预分配 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 个 Vector3lightProbe.array),下标 0..8 分别对应球谐 band 0(1 个直流项)、band 1(3 个线性项)与 band 2(5 个二次项)的系数。着色器侧正是通过 this.lightProbe.element( i ) 逐项访问这些系数的。

除自有属性外,LightProbeNode 还从 AnalyticLightNode 继承了一组在调试 / 二次开发中常用的字段:light(光源引用)、colorColor)、colorNode(颜色节点)、shadowNodeisAnalyticLightNode 等。对 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 );

	}

}

这一段揭示了探针亮度的真实管线:

  1. 先调用父类 super.update(frame) 更新基础颜色;
  2. 循环拷贝 9 组球谐系数 light.sh.coefficients[i]lightProbe.array[i]
  3. 每组系数都乘以 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 );

}

拆解其语义:

  1. normalWorld:世界空间法线访问器,是评估某点辐照度所需的方向输入;
  2. getShIrradianceAt( normalWorld, this.lightProbe ):把 9 组球谐系数与法线方向求值,输出一个 vec3 辐照度颜色;
  3. 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] 组合项 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.htmlexamples/webgpu_lightprobes.html)。

调试与常见问题

  1. 看不到探针效果? 确认使用的是节点化渲染后端(WebGPU 或 three.webgpu.js)。经典 WebGL 渲染器走的是 shader chunk 管线而非 LightProbeNode。
  2. 想实时更新环境? LightProbeNode 每帧在 update 中从 light.sh.coefficients 拷贝数据,因此直接改写 probe.sh(例如 WebXR 光估计回调中替换 SH 数据)即可生效,无需重建材质或节点。
  3. 亮度差异如何调? 探针 intensity 会以 multiplyScalar 形式作用于全部 9 组系数(LightProbeNode.js),调大即可整体增强辐照度;也可同时结合场景中的环境贴图间接光做对比。
  4. 与普通光源叠加方式不同。 LightProbeNode 只贡献 context.irradiance(间接漫反射累积器),不会走 setupDirect 的直接光路径,也不会产生阴影,这与点光/聚光节点的行为存在本质差异,排查光照不匹配时需要留意。

小结

LightProbeNode 是 three.js 节点光照体系中把「球谐光照探针」接入间接漫反射计算的唯一入口节点。它以 9 个 Vector3UniformArrayNode 承载三阶球谐系数,在 update() 中每帧将 light.sh.coefficientsintensity 同步到 GPU uniform,并在 setup() 中借助 getShIrradianceAt 依据世界法线重建辐照度,累加进 context.irradiance,最终由 PBR / Phong / Toon 等光照模型的 indirectDiffuse 路径使用。理解这条「LightProbe → LightProbeNode → context.irradiance → 光照模型」的链路,是掌握 three.js WebGPU 光照架构以及调试真实环境光(含 WebXR 光估计)的重要基础。

延伸阅读

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

项目优选

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