首页
/ three.js LightProbe 光照探针完全指南:基于球谐函数(Spherical Harmonics)的环境光照明

three.js LightProbe 光照探针完全指南:基于球谐函数(Spherical Harmonics)的环境光照明

2026-09-07 21:37:57作者:羿妍玫Ivan

三维场景中的物体并非只能由点光、平行光、聚光灯这类“主动发光光源”照亮。three.js 提供了一种截然不同的照明思路——LightProbe(光照探针):它本身不发光,而是存储光线穿过三维空间时的信息,渲染时用这些数据近似照射到物体上的光线。本文以 docs/pages/LightProbe.html.md 的 LightProbe 文档为主线,深入源码剖析其球谐编码原理、构造器与属性、序列化行为,并结合 LightProbeGeneratorLightProbeHelper、WebXR 光照估计与仓库内真实示例,带读者掌握从环境贴图生成探针并接入场景的完整实战流程。

什么是光照探针:与经典光源的本质区别

经典的 three.js 光源(DirectionalLightPointLightSpotLight 等)会主动发射光线照亮场景。而 LightProbe 恰恰相反——它存储的是空间中某一点各个方向入射光的分布信息。渲染时,照射到 3D 物体表面的光通过读取探针数据来近似还原,从而让物体呈现出仿佛被环境所包裹的、各方向连续的漫反射照明效果。

src/lights/LightProbe.js 的类注释可以看到官方对它的精确定义:

  • 光照探针通常由辐射度环境贴图(radiance environment map)生成;
  • 光照估计数据也可以来自其他来源,例如 WebXR 的真实光照估计,从而渲染出对现实世界光照做出反应的增强现实(AR)内容;
  • 当前探针实现支持的是漫反射光照探针(diffuse light probe),它在功能上等价于一张辐照度环境贴图(irradiance environment map)。

这一行注释点明了 LightProbe 的使用边界:它适合表达低频、平滑的环境漫反射光(天光、室内环境光、半球渐变光),并不适合表达高频细节(如镜面高光反射点),后者仍需要借助反射/环境贴图或 PBR 材质自身完成。

继承关系:从 EventDispatcher 到 Object3D 再到 Light

LightProbe 的继承链为 EventDispatcher → Object3D → Light → LightProbe(文档开头即给出了该继承链),这意味着它天生具备三维空间中所有对象的能力:位置、旋转、缩放、父子关系、可见性开关,以及事件派发机制。

关键点在于,构造器内部调用的是 super( undefined, intensity )(见 src/lights/LightProbe.js),即向父类 Light 传入了 undefined 颜色——因为探针的颜色并不由单一 color 决定,而是完整编码在球谐系数里。从源码 src/lights/Light.js 可见,Light 基类本身负责 colorintensity 两个属性,LightProbe 继承了这两者。

值得注意的工程细节:LightProbe.position 不参与场景光照计算。仓库示例 examples/webgl_lightprobe.html 中明确写了注释:

lightProbe.position.set( - 10, 0, 0 ); // position not used in scene lighting calculations (helper honors the position, however)

也就是说探针的球谐数据本身是全方向全局的“零阶空间近似”,移动探针位置只影响可视化 helper 的显示位置,并不改变被照亮的物体。若要表达“空间上不同位置、不同光照”的差异,需要使用更高阶的方案(如仓库中 examples/jsm/lighting/LightProbeGridWebGL.js 之类的光照探针网格),本类的单探针模型更适合整体环境基调照明。

构造函数签名与参数

new LightProbe( sh : SphericalHarmonics3, intensity : number )

const probe = new THREE.LightProbe( sh, 1 );
参数 类型 默认值 说明
sh SphericalHarmonics3 new SphericalHarmonics3() 编码了光照信息的球谐函数对象
intensity number 1 光的强度(强度系数)

参数 intensity 同样会传递给父类作为 Light.intensity。当你不传入 sh 时(例如 new THREE.LightProbe()),它会自动新建一个全零系数的 SphericalHarmonics3,此时探针不产生任何照明贡献——在 examples/webgl_lightprobe.html 中正是先创建空探针,待环境贴图异步加载完成后,再用 lightProbe.copy( LightProbeGenerator.fromCubeTexture( cubeTexture ) ) 填充数据。

属性详解

.isLightProbe : boolean(只读)

类型检测标记,始终为 true。源码 src/lights/LightProbe.js 中在构造器里写死该值。它与 three.js 其他类的 is* 标记遵循同一惯例(如 isLightisMesh),用于在运行时通过鸭子类型判断对象种类:

if ( obj.isLightProbe ) { /* 这是一个光照探针 */ }

.sh : SphericalHarmonics3

探针数据本体。光照探针使用球谐函数编码光照信息,这是一个包含 9 个 Vector3 系数的三阶(order-3)球谐。该属性在构造器中按引用保存传入对象:

this.sh = sh;

从仓库单元测试 test/unit/src/lights/LightProbe.tests.js 可见,测试仅验证两件事:LightProbe 是 Light 的子类、isLightProbe 恒为 true。这也说明该类的核心行为几乎全部委托给了 SphericalHarmonics3 与父类 Light

继承自父类的常用属性

  • .color : Color:继承自 Light。创建时被置为 undefined 传入,实际通常不直接使用;多数示例会给物体本身设置材质颜色。
  • .intensity : number:强度系数,默认 1,可放大或缩小探针整体亮度贡献。
  • .position / .rotation / .scale / .visible 等:继承自 Object3D,如前所述 position 不参与照明计算。

源码级深解:SphericalHarmonics3 如何编码光照

LightProbe 的照明能力完全建立在 src/math/SphericalHarmonics3.js 之上。该类“表示三阶球谐函数(SH),LightProbe 用它编码光照信息”,其数学依据是环境贴图辐射度论文(Ramamoorthi & Hanrahan 的 An Efficient Representation for Irradiance Environment Maps)与 Sloan 等人的 Stupid SH Tricks(源码注释中引用了这两份资料)。

9 个系数的分布

构造器生成 9 个 Vector3 系数,对应球谐的 band 0(1 个)、band 1(3 个)、band 2(5 个),即“频率从低频到略高频”的三阶截断:

this.coefficients = [];
for ( let i = 0; i < 9; i ++ ) {
    this.coefficients.push( new Vector3() );
}

采样与重构

  • getAt( normal, target ):给定单位法线方向,重建该方向上的辐射度 radiance(线性组合 band 0/1/2 系数与各阶基函数值,乘以常数系数)。
  • getIrradianceAt( normal, target ):给定法线,返回该方向上的辐照度 irradiance,即“辐射度与余弦瓣(cosine lobe)的卷积”。系数换算因子在源码注释中一一给出(如 band 0 使用 0.886227 = π * 0.282095)。
  • static getBasisAt( normal, shBasis ):求出方向对应的 9 个球谐基函数值,供系数投影使用。

光照探针渲染时使用的正是辐照度重构——这也印证了文档所述“漫反射光照探针等价于辐照度环境贴图”:一个三阶 SH 辐照度表示已经足以高精度逼近任意低频环境光照在朗伯表面上的效果,这正是该算法被选作探针编码的原因。

常用工具方法

SphericalHarmonics3 还提供 setzeroaddaddScaledSHscalelerpequalscopyclonefromArray / toArray。其中 lerp 支持两套 SH 数据之间线性插值——这是实现“多个探针结果之间平滑过渡”的底层能力;fromArray/toArray 则用于与 flat 数组互换,直接服务于 LightProbe.toJSON 的序列化。

实例方法:copy 与 toJSON

LightProbe 只重写了两个实例方法(src/lights/LightProbe.js):

.copy( source : LightProbe ) : this

先调用 super.copy( source ) 复制父类属性(含 colorintensity,见 src/lights/Light.js),随后执行 this.sh.copy( source.sh ) 将 9 个球谐系数逐一复制到本对象。注意是深拷贝系数数组,而非共享引用,因此复制后的探针可以独立调整。

.toJSON( meta ) : Object

在父类 JSON 基础上,将球谐数据追加进序列化结果:

data.object.sh = this.sh.toArray();

sh.toArray() 将 9 个 Vector3(27 个 float)展平为普通数字数组(见 src/math/SphericalHarmonics3.js),使探针可以随场景一起被编辑器或序列化工具持久化。配合 fromArray 即可无损还原——这是 three.js 官方编辑器 / GLTF 导出链路中场景对象能被完整保存的重要一环。

从环境贴图生成探针:LightProbeGenerator

通常你不会手写 9 个 SH 系数,而是让 three.js 从环境贴图中替你计算。仓库提供工具类 examples/jsm/lights/LightProbeGenerator.js,以 three/addons/lights/LightProbeGenerator.js 方式导入:

import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js';

LightProbeGenerator.fromCubeTexture( cubeTexture ) : LightProbe

输入以 CubeTexture 形式表示的(辐射度)环境贴图,返回 LightProbe。其底层算法(源码注释引用 Stupid SH36)值得展开:

  1. 遍历 6 个 cube 面,将每个面的位图绘制到 Canvas 上并读取 RGBA 像素数据;
  2. 颜色先转为线性色彩空间(根据 cubeTexture.colorSpace,仅 SRGBColorSpace 需要转换,见辅助函数 convertColorToLinear);
  3. 由像素行列坐标映射到单位立方体表面的坐标,并据此计算该像素对应的立体角权重(weight = 4 / ( sqrt( lengthSq ) * lengthSq )),保证 cubemap 各方向采样面积均匀;
  4. 对该方向调用 SphericalHarmonics3.getBasisAt() 得到 9 个基函数值,按 RGB 三通道累加到对应系数;
  5. 遍历结束后用 norm = ( 4 * Math.PI ) / totalWeight 归一化,最终 return new LightProbe( sh )

由于像素级投影较耗时,实践中常用较低分辨率的 PMREM/cubemap 生成探针;生成后可立即 .dispose() 不再需要的大图。

LightProbeGenerator.fromCubeRenderTarget( renderer, cubeRenderTarget ) : Promise(异步)

输入以 CubeRenderTarget 表示的辐射度环境贴图(即“把环境渲染到立方体渲染目标再读回”),返回 Promise。典型场景是运行时动态生成环境(如反射探针的 CubeCamera)。方法源码的关键约束与细节:

  • 立方体渲染目标的纹理必须为 RGBA 格式texture.format 应为 RGBAFormat),否则 readRenderTargetPixels 无法读取像素;
  • 支持 FloatType / HalfFloatType / UnsignedByteType 三类纹理数据类型;半浮点数据会经 DataUtils.fromHalfFloat 解码;
  • 根据渲染器坐标系(WebGLCoordinateSystem 与 WebGPU 默认坐标系不同)翻转采样的 flip 因子,源码中用 renderer.coordinateSystem 判断——因此 WebGL 与 WebGPU 渲染器都可用;
  • WebGL 渲染器路径使用 readRenderTargetPixelsAsync( ... , faceIndex ),其他路径传参签名不同,但都会逐面异步读回 6 个面。

简易用法对比

// 方式一:从 cube 纹理(异步加载完成之后调用)
const probeFromTex = LightProbeGenerator.fromCubeTexture( cubeTexture );

// 方式二:从 cube 渲染目标(异步读取)
const probeFromRT = await LightProbeGenerator.fromCubeRenderTarget( renderer, cubeRenderTarget );

借助 WebXR 获得真实世界光照

文档特别强调:光照估计数据也可以由 WebXR 等外部渠道提供,这让 AR 内容能对真实环境光照做出反应。仓库中的 examples/jsm/webxr/XREstimatedLight.js 正是这一机制的落地实现——它组合一个 DirectionalLight 与一个 LightProbe,通过 XRWebGLBinding.getReflectionCubeMap() 获取真实世界的反射立方图,并在 XR frame 中读取 xrFrame.getLightEstimate( this.lightProbe ) 更新光照数据;lightProbe 还会派发 reflectionchange 事件通知外部刷新反射纹理。也就是说,同一个 LightProbe 实例既是 WebXR API 的数据载体,也负责驱动场景漫反射照明。

可视化调试:LightProbeHelper

调参时肉眼难以判断探针“长什么样”,可以用 helper 把它渲染成一个彩色球体。仓库提供 examples/jsm/helpers/LightProbeHelper.js,基于 MeshShaderMaterial 实现:片元着色器把 9 个 SH 系数按辐照度公式(shGetIrradianceAt)重建每个法线方向的出射光并绘制到球面上,相当于把探针的光场“可视化采样”出来:

import { LightProbeHelper } from 'three/addons/helpers/LightProbeHelper.js';

const helper = new LightProbeHelper( lightProbe, 1 ); // size=1
scene.add( helper );

注意事项(来自其源码注释与实现):

  • 该 WebGL 版本只能配合 WebGLRenderer;使用 WebGPURenderer 时应改从 three/addons/helpers/LightProbeHelperGPU.js 导入;
  • helper 会跟随探针的 .positiononBeforeRender 中每帧同步位置与 intensity,见 examples/jsm/helpers/LightProbeHelper.js),所以调试时移动探针能看到小球位置变化;
  • 不再使用时调用 helper.dispose() 释放几何体与材质对应的 GPU 资源。

实战:把 LightProbe 接入场景的完整流程

仓库中最直接的示例是 examples/webgl_lightprobe.html(含 WebGPU 对照版 examples/webgpu_lightprobe.html)。其核心流程可作为标准范式:

// 1. 先创建空探针并加入场景
lightProbe = new THREE.LightProbe();
scene.add( lightProbe );

// 2. 异步加载环境 cubemap,生成探针数据并回填
lightProbe.copy( LightProbeGenerator.fromCubeTexture( cubeTexture ) );
lightProbe.intensity = api.lightProbeIntensity;   // 可调强度

// 3.(可选)用 helper 可视化
const helper = new LightProbeHelper( lightProbe, 1 );
scene.add( helper );

完整的业务代码请直接查看 examples/webgl_lightprobe.html。同目录还提供了“真实房间光探针阵列”进阶示例 examples/webgl_lightprobes.html 与 Sponza 场景版 examples/webgl_lightprobes_sponza.html:它们用 LightProbeGridWebGL 在房间内布置密集探针网格并结合阴影,展示多探针空间变化照明;相关实现位于 examples/jsm/lighting/LightProbeGridWebGL.js,此类高级玩法可作为单探针理解成熟后的延伸阅读。

需要提醒的使用限制:

  1. LightProbe 是低频漫反射照明方案,不负责镜面高光,常与方向光等主动光源配合以获得有立体感又受环境包裹的画面;
  2. position 不影响照明结果,若需空间渐变光感应使用探针网格;
  3. 从 CubeTexture 生成需要像素级遍历,建议离线/低分辨率计算或仅在需要时执行;
  4. 从 CubeRenderTarget 生成务必确保纹理为 RGBA,并注意 WebGL/WebGPU 坐标系差异(该差异已由库内部处理)。

小结

LightProbe 是 three.js 中对“环境漫反射照明”的标准抽象:以 9 个三阶球谐系数编码空间光场,功能上等价于辐照度环境贴图。配合 LightProbeGenerator 可从 CubeTexture / CubeRenderTarget 一键生成,配合 LightProbeHelper 可直观调试,配合 WebXR 光估计甚至能捕捉真实世界光照用于 AR 渲染。理解 src/lights/LightProbe.js 与其数据载体 src/math/SphericalHarmonics3.js,再对照 examples/webgl_lightprobe.htmlexamples/webgl_lightprobes_sponza.html,即可从“能用”进阶到“会用”,让三维场景获得真实、连续且开销可控的环境光响应。

相关资源索引

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 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
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
918
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.6 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
517
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389