首页
/ three.js 辐照度探针网格 LightProbeGrid 完全指南:GPU 全烘焙 L2 球谐全局光照的原理与用法

three.js 辐照度探针网格 LightProbeGrid 完全指南:GPU 全烘焙 L2 球谐全局光照的原理与用法

2026-09-07 13:25:05作者:羿妍玫Ivan

LightProbeGrid 是 three.js 中一类"3D 探针网格"光源:它以规则三维网格布置 L2 球谐(Spherical Harmonic)辐照度探针,为整个空间提供随位置连续变化的漫反射全局光照,适合室内、封闭空间或需要柔和间接光的场景。本文以 docs/pages/LightProbeGrid.html.md 为骨架,结合仓库中的实现源码与官方示例,完整讲解其构造参数、烘焙选项、图集(atlas)存储布局、GPU 烘焙管线、运行时采样原理与多房间部署实战,读完即可在自己的场景中接入并调优这套探针光照方案。

light probe 体积渲染示例的官方运行截图(单体积版)

多房间、多探针体积同时部署的官方运行截图

一、LightProbeGrid 是什么:位置相关的漫反射全局光照

按文档定义,LightProbeGrid 是一组"L2 球谐辐照度探针的三维网格(A 3D grid of L2 Spherical Harmonic irradiance probes)",用于提供随位置变化的漫反射全局光照(position-dependent diffuse global illumination)。

与传统的单点 LightProbe 或场景级 HemisphereLight 相比,它解决的核心问题是光照在空间中的非均匀性:例如红色墙壁会把附近物体染上暖色、门窗与角落的光照显著不同。LightProbeGrid 在长方体体积内按 widthProbes × heightProbes × depthProbes 均匀布置采样探针,每个探针处烘焙 9 个 L2 球谐系数描述的辐照度分布,渲染时在体积内做三维插值采样,从而近似真实间接光。

其烘焙数据全部保存在单个 RGBA 3D 纹理图集中(文档与源码使用 texture-atlas 布局沿 Z 轴堆叠),烘焙过程完全驻留 GPU(GPU-resident):cubemap 渲染、SH 投影、纹理打包均发生在 GPU 上,零 CPU 回读(zero CPU readback),因此不会阻塞主线程。

二、渲染后端与仓库中的两套实现

WebGLRenderer 专用版本(文档主体的接口定义)

文档明确指出"该类只能配合 WebGLRenderer 使用"(A version for WebGPURenderer will be added at a later point)。这一行注释在仓库中实际对应的是 WebGL 实现 examples/jsm/lighting/LightProbeGridWebGL.js(类名 LightProbeGridWebGL),其成员接口与本文档页面完全一致:构造参数、属性、bake() 选项与图集布局说明逐句对应,数据存储于单个 RGBA WebGL3DRenderTarget 中。

WebGPURenderer 版本(已落地)

文档所述"WebGPU 版后续加入"目前已实现:examples/jsm/lighting/LightProbeGrid.js 中的 LightProbeGrid 现在是 WebGPURenderer 版本(内部基于 TSL 节点烘焙,详见后文),源码头注释明确写道:"This is the WebGPURenderer version of LightProbeGrid… When using WebGLRenderer, import the grid from LightProbeGridWebGL.js instead." 两套实现共享完全相同的构造签名、属性和烘焙语义,仅底层渲染目标与着色器技术栈不同(WebGL3DRenderTarget + GLSL 对应 WebGL 版;RenderTarget3D + Node/TSL 对应 WebGPU 版)。

实际集成时的选择规则很简单:WebGLRenderer → LightProbeGridWebGL,WebGPURenderer → LightProbeGrid。官方示例 examples/webgl_lightprobes.html 导入的是前者,examples/webgpu_lightprobes_complex.html 导入的是后者。

三、安装与导入

LightProbeGrid 是 addon(扩展模块),必须显式导入,不会包含在 three 核心构建中。通过 importmap 或构建工具将 three/addons/ 指向仓库的 examples/jsm/ 目录即可:

// 文档给定导入路径
import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';

结合上节的实现分工,在实际工程中通常写成:

// WebGLRenderer 环境
import { LightProbeGridWebGL } from 'three/addons/lighting/LightProbeGridWebGL.js';

// WebGPURenderer 环境
import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';

相关辅助类 LightProbeGridHelper / LightProbeGridHelperWebGL 位于 examples/jsm/helpers/LightProbeGridHelper.js 与同目录 LightProbeGridHelperWebGL.js

四、构造函数与探针布局

构造签名与参数默认值

new LightProbeGrid( width, height, depth, widthProbes, heightProbes, depthProbes ),体积中心位于对象(grid)的 position 处。完整参数见下表:

参数 含义 默认值
width 体积沿 X 方向的全宽 1
height 体积沿 Y 方向的全高 1
depth 体积沿 Z 方向的全深 1
widthProbes X 方向探针数 Math.max( 2, Math.round( width ) + 1 )
heightProbes Y 方向探针数 Math.max( 2, Math.round( height ) + 1 )
depthProbes Z 方向探针数 Math.max( 2, Math.round( depth ) + 1 )

三个 *Probes 参数可省略;从源码 constructorexamples/jsm/lighting/LightProbeGrid.js)可见,省略时按边长四舍五入 +1 并保证至少为 2。该默认策略保证了任意边长下体积内插值至少有两层样本,不会退化为单点。构造后探针数被整理为 Vector3 存入 .resolution

示例:构造一个覆盖 8×5×8 世界单位、每轴 6 个探针的体积:

const grid = new LightProbeGrid( 8, 5, 8, 6, 6, 6 );
grid.position.set( 0, 2.5, 0 );   // 体积中心

探针位置与 bounding box

探针沿各轴均匀铺满整个体积:当某轴探针数大于 1 时,相邻探针间距为 w / ( res - 1 ),首尾探针恰好落在体积边界上。getProbePosition( ix, iy, iz, target ) 返回网格索引 (ix, iy, iz) 对应探针的世界空间位置(写入传入的 target 向量并返回),对应实现可参看源码的公式(examples/jsm/lighting/LightProbeGridWebGL.js):

target.set(
    res.x > 1 ? pos.x - w / 2 + ix * w / ( res.x - 1 ) : pos.x,
    res.y > 1 ? pos.y - h / 2 + iy * h / ( res.y - 1 ) : pos.y,
    res.z > 1 ? pos.z - d / 2 + iz * d / ( res.z - 1 ) : pos.z
);

updateBoundingBox() 用当前位置与宽高深调用 boundingBox.setFromCenterAndSize() 重建世界空间包围盒;.boundingBox 会在 bake() 时被自动更新。

五、属性速览

属性 类型 说明
.boundingBox Box3 网格的世界空间包围盒,由 bake() 自动更新
.depth number 体积沿 Z 方向全深
.height number 体积沿 Y 方向全高
.width number 体积沿 X 方向全宽
.isLightProbeGrid boolean(只读) 类型测试标志,恒为 true
.resolution Vector3 各轴探针数量
.texture Data3DTexture 存储全部 7 个打包 SH 子体积的单个 RGBA 3D 图集纹理,烘焙前为 null

WebGPU 版还额外暴露了 .falloff(默认 0):以世界单位为单位的"体积边界外衰减距离"。0 表示贡献在所有位置生效(钳制到边界内,等价于单体积用法);设为小正值即可让多个互相重叠的网格平滑融合(源码注释见 examples/jsm/lighting/LightProbeGrid.js)。

此外 grid 本身是 Light 的子类(WebGL 版继承 Object3D 并被渲染器当作探针光源处理),可调节 .intensity 缩放最终辐照度贡献,并通过 .visible 开关整体启用/停用 GI。

六、烘焙 API:bake()

bake( renderer, scene, options )每个探针位置渲染 cubemap,并将其投影为 L2 SH 系数。可选 options 的完整定义如下:

选项 默认值 说明
cubemapSize 8 每个 cubemap 面的分辨率
near 0.1 立方体相机的近平面
far 100 立方体相机的远平面
bounces 0 直接光之外额外追加的反弹 pass 数
sampleCount(仅 WebGPU 版) 512 SH 投影时每个探针沿等面积方向采样积分的采样方向数

调用示例(参考官方示例 examples/webgl_lightprobes.html):

const probes = new LightProbeGridWebGL( 5.6, 4.7, 5.6, res, res, res );
probes.position.set( 0, 2.5, 0 );
probes.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } );
scene.add( probes );

多次反弹的语义

默认 bounces: 0 只捕获直接光。每追加一个 bounce pass,该 pass 会把上一 pass 烘焙出的图集当作间接光源再次采样——关键前提是 grid 在烘焙前已被加入场景scene.add),这样后续 pass 渲染 cubemap 时图集贡献才会进入画面,从而每个额外 pass 累积一次反弹(bounces: 1 即直接光 + 1 次间接反弹)。源码通过控制 grid 自身可见性实现这一点:pass 0 时 this.visible = false(避免读到未初始化的图集),之后每个 pass 恢复可见(examples/jsm/lighting/LightProbeGridWebGL.js)。

渲染器前提与烘焙期间的状态管理

  • WebGL 版:无特殊前置要求;烘焙期间源码会临时关闭 renderer.shadowMap.autoUpdate(灯光在烘焙中不移动,只强制首帧更新一次阴影贴图);若场景中存在投射阴影的 SunLight,会因它的阴影级联按活动相机拟合、无法在探针渲染间冻结,而被临时替换为等价的 DirectionalLight,结束后还原(examples/jsm/lighting/LightProbeGridWebGL.js)。
  • WebGPU 版:要求 renderer.isWebGPURenderer === true 且渲染器已完成 await renderer.init(),否则 bake() 直接抛错;源码同时要求场景矩阵只更新一次(烘焙期间关闭 scene.matrixWorldAutoUpdate),并在结束时恢复全部渲染器/场景状态(examples/jsm/lighting/LightProbeGrid.js)。

因此 WebGPU 环境必须先 await renderer.init() 再调用 bake()(见 examples/webgpu_lightprobes_complex.html)。此外 WebGPU 版 bake() 内部会幂等地把 LightProbeGridNode 注册进 renderer.library 的光类型映射。

七、GPU 全烘焙管线深入

整个烘焙由三个 GPU 环节组成,全程零 CPU 回读,源码结构清晰可循(WebGPU 版 examples/jsm/lighting/LightProbeGrid.js,WebGL 版同理)。

第一步:逐探针渲染 cubemap

iz → iy → ix 遍历所有 widthProbes × heightProbes × depthProbes 个探针,把共享的 CubeCamera 移到 getProbePosition() 给出的世界坐标,调用 cubeCamera.update( renderer, scene ) 渲染六面 cubemap。共享的 CubeRenderTargetCubeCamera 在模块级按 (cubemapSize, near, far) 键池化,避免每次烘焙重建分配。

第二步:SH 投影到"批量"纹理

每个探针的 cubemap 被积分投影成 9 个 L2 系数,写入一块宽 9 像素、高 totalProbes 的 2D "批量纹理"(batch target),每个探针占一行、每列对应一个系数

投影采样策略因后端而异:

  • WebGPU 版使用 TSL 的 projectSHNode():沿等面积 Fibonacci 球面sampleCount(默认 512)个方向做数值积分,用黄金角增量生成均匀分布的方向(源码 examples/jsm/lighting/LightProbeGrid.js),每个方向权重 4π / sampleCount
  • WebGL 版直接在片元着色器中遍历 cubemap 全部 6×CUBEMAP_SIZE² 个像素,逐面用解析权重 4 / (r·r²) 修正面片面积(examples/jsm/lighting/LightProbeGridWebGL.js)。

两条路径都使用同一套 9 个 L2 基函数常数(band 0:0.282095;band 1:0.488603·(y,z,x);band 2:对应 xy,yz,3z²-1,xz,x²-y² 的高阶项)。

第三步:打包进 3D 图集

投影得到的 9 个系数是 9 个 vec3(27 个浮点),而单个 RGBA 纹理的一条 texel 只有 4 个通道。打包策略是:把 9 个系数拆成 7 段,各存一段到 7 个 RGBA 子体积(sub-volume) 中,沿 Z 轴堆叠成单一 3D 纹理。源码中每个子体积 t 的片元值由 repackNode()/TEXTURE_INDEX 分支决定(examples/jsm/lighting/LightProbeGridWebGL.js):

// 子体积 t = 0..6,RGBA 四个通道依次存放(c0..c8 为 9 个 SH 系数)
// 0: (c0.r, c0.g, c0.b, c1.r)
// 1: (c1.g, c1.b, c2.r, c2.g)
// 2: (c2.b, c3.r, c3.g, c3.b)
// 3: (c4.r, c4.g, c4.b, c5.r)
// 4: (c5.g, c5.b, c6.r, c6.g)
// 5: (c6.b, c7.r, c7.g, c7.b)
// 6: (c8.r, c8.g, c8.b, 0.0)

合计 7×4 = 28 个通道恰好容纳 27 个系数浮点 + 1 个闲置 alpha。打包阶段本质是"GPU 到 GPU"的重排:以整屏 quad 逐 slice 渲染,把批量纹理中每行系数读回后写进 3D 图集对应 slice。

图集 padding 布局(防止三线性滤波渗色)

这是文档用较大篇幅强调的布局要点,必须原样理解:每个子体积占据 ( nz + 2 ) 个图集切片——两头各加 1 个 padding 切片,padding 内容是相邻边界数据切片的拷贝。这样当硬件做三线性(trilinear)滤波、采样跨越子体积边界时,不会把相邻子体积的无关数据插值进来造成颜色渗漏(color bleeding)。

nz = resolution.zPADDING = 1 为例,文档给出的完整布局如下:

slice   0              : padding  (copy of sub-volume 0, data slice 0)
  slices  1 … nz         : sub-volume 0 data
  slice   nz + 1         : padding  (copy of sub-volume 0, data slice nz-1)
  slice   nz + 2         : padding  (copy of sub-volume 1, data slice 0)
  slices  nz+32*nz+2  : sub-volume 1 data
  …

图集总深度 = 7 * ( nz + 2 )。源码实现中用 ATLAS_PADDING = 1 常量与公式 paddedSlices = nz + 2 * ATLAS_PADDINGatlasDepth = 7 * paddedSlices 创建 WebGL3DRenderTarget / RenderTarget3Dexamples/jsm/lighting/LightProbeGridWebGL.js),并在 repack 阶段分别写出每个子体积的 leading padding(数据切片 0 的拷贝)、全部数据切片与 trailing padding(数据切片 nz-1 的拷贝)。烘焙产生的单个纹理即 .texture 指向的对象。

八、运行时采样与光照集成

烘焙完成后,把 grid 加入场景即可自动影响光照,无需手动采样——渲染器/光照系统在每个片元上按世界位置对图集做三维插值,按法线评估 L2 辐照度。

WebGPU:LightProbeGridNode

examples/jsm/tsl/lighting/LightProbeGridNode.js 中的 LightProbeGridNode 继承自 AnalyticLightNodesetup() 中(对应源码 L94-L132)完成三件事:

  1. boundingBox 计算采样坐标:将世界位置沿法线偏移半个探针间距(缓解法线自交导致的表面"贴脸"采样误差),再映射到图集的 texel 中心坐标,即 uvw = (samplePos - min) / range 的 clamp 与缩放;
  2. evaluateGridIrradiance() 按 Z 定位到 7 个子体积各自的起始 slice,用 getShIrradianceAt() 解出 L2 辐照度并 clamp 非负;
  3. 结果乘 .intensitybuilder.context.irradiance.addAssign() 累加进光照上下文——因此所有标准 Node 材质自动获得该 grid 的光照

每个 grid 还带有 4 个可交换 uniform(_min/_max/_resolution/_intensity/_falloff),在 update() 中逐帧同步,材质只需编译一次。

WebGL:lights_fragment_begin 集成

WebGL 管线在片元着色器侧做等价处理:着色器 chunk src/renderers/shaders/ShaderChunk/lightprobes_pars_fragment.glsl.js 中声明了 getLightProbeGridIrradiance( worldPos, worldNormal ),由 src/renderers/webgl/WebGLRenderer.jssrc/renderers/webgl/WebGLPrograms.js 在收集渲染状态时把场景中的 LightProbeGridWebGL 当作光源,在材质片段中按世界坐标采样图集并叠加辐照度。

多网格叠加与 falloff

由于每个 grid 是独立的"光源/光照贡献",场景可同时存在多个体积(每个房间一个)。WebGPU 版示例 examples/webgpu_lightprobes_complex.html 给每个网格设 falloff = 1.0,让贡献在体积边界外 1 个世界单位内平滑衰减到 0,从而两个房间独立烘焙、互不渗色地融合。WebGL 版 examples/webgl_lightprobes_complex.html 则不设 falloff,演示通过场景移除/恢复来控制体积。

九、可视化调试:LightProbeGridHelper

为了确认烘焙结果,three.js 提供配套辅助类 LightProbeGridHelper(对应文档页 docs/pages/LightProbeGridHelper.html),它在每个探针位置渲染一个球体,球体颜色即该探针 L2 SH 估算的辐照度:

const helper = new LightProbeGridHelper( probes );   // 第二个参数 sphereSize 默认 0.12
scene.add( helper );

从源码看,Helper 是一次 InstancedMesh 单次 draw call 完成全部探针球绘制;其片元着色器同样以 instance 坐标采样 7 个子体积并通过 getShIrradianceAt 着色(examples/jsm/helpers/LightProbeGridHelper.js)。网格重烘焙或更换后调用 helper.update() 重建实例矩阵与 UVW 属性;helper.dispose() 释放几何与材质。调试时把探针球逐帧暴露在画面中,可直观检查烘焙是否覆盖期望区域。

十、实战:从单体积到多房间

最小完整流程(WebGLRenderer)

参照 examples/webgl_lightprobes.html 的骨架:

import * as THREE from 'three';
import { LightProbeGridWebGL } from 'three/addons/lighting/LightProbeGridWebGL.js';
import { LightProbeGridHelperWebGL } from 'three/addons/helpers/LightProbeGridHelperWebGL.js';

// …创建场景、网格体与普通光源…

const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;

const probes = new LightProbeGridWebGL( 5.6, 4.7, 5.6, 6, 6, 6 );
probes.position.set( 0, 2.5, 0 );
probes.bake( renderer, scene, { cubemapSize: 32, near: 0.05, far: 20 } );
scene.add( probes );

const helper = new LightProbeGridHelperWebGL( probes );
helper.visible = false;          // 调试时可切换显示
scene.add( helper );

LightProbeGridWebGL/LightProbeGridHelperWebGL 换成 WebGPU 版本,并在 bake() 之前加上 await renderer.init(),即为 WebGPU 等价流程。

多房间 multi-volume 工作流

官方示例 examples/webgl_lightprobes_complex.html(及 WebGPU 版 examples/webgpu_lightprobes_complex.html)演示了两个房间各放一个独立体积的做法,其关键经验可总结为:

  1. 重新烘焙前先从场景移除旧网格scene.remove + dispose()),防止旧图集作为光源影响新烘焙(反馈);
  2. 分别烘焙两个体积——左右各 new LightProbeGridWebGL( 7.8, 4.7, 7.6, resolution, resolution, resolution ),中心定在各房间中央,bake 使用 { cubemapSize: 32, near: 0.05, far: 20 }
  3. 烘焙完成后再把网格加入场景(注意 bounces 场景则需先 add 再 bake,见第六节);
  4. 更新调试可视化:已有 helper 时重新绑定 helper.probes = 新网格 并调用 update()
  5. 示例 GUI 提供了 2–12 可调的 resolution(探针分辨率)与 "Show Probes" 开关,可即时观察采样密度对烘焙质量的影响——探针越多细节越丰富、烘焙与存储开销也越大。

生命周期与资源释放

网格不再使用时调用 grid.dispose(),它会释放内部 3D 渲染目标并把 .texture 置回 null(WebGPU 版同时调用父类 Light.dispose());Helper 用毕调用 helper.dispose()。烘焙用的共享 cubemap、批量纹理与材质在模块级按尺寸池化复用(rebake 不产生新的 GPU 分配),这一点从两套实现的 _ensure* 缓存函数可见,多网格、多次重烘焙场景下可以放心反复调用 bake()

十一、小结

LightProbeGrid 把"每帧大量计算间接光照"转化为"烘焙期的一次性 GPU 计算 + 运行期一次 3D 纹理插值":9 个 L2 SH 系数被紧凑打包进单张 RGBA 3D 图集的 7 个子体积,padding 切片保证三线性滤波不跨子体积渗色;WebGL 与 WebGPU 两套后端共享同一接口与图集语义;配合 falloff 可实现多房间/多体积的无缝融合。无论是封闭室内场景的柔和 GI,还是需要随位置变化的漫反射间接光,这套方案都提供了低运行期开销、易调试(Helper 可视化)的实用路径。想进一步验证可运行 examples/webgl_lightprobes.htmlexamples/webgl_lightprobes_complex.htmlexamples/webgpu_lightprobes_complex.html,并对照 examples/jsm/lighting/LightProbeGridWebGL.jsexamples/jsm/tsl/lighting/LightProbeGridNode.js 深入阅读其实现细节。

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