three.js 辐照度探针网格 LightProbeGrid 完全指南:GPU 全烘焙 L2 球谐全局光照的原理与用法
LightProbeGrid 是 three.js 中一类"3D 探针网格"光源:它以规则三维网格布置 L2 球谐(Spherical Harmonic)辐照度探针,为整个空间提供随位置连续变化的漫反射全局光照,适合室内、封闭空间或需要柔和间接光的场景。本文以 docs/pages/LightProbeGrid.html.md 为骨架,结合仓库中的实现源码与官方示例,完整讲解其构造参数、烘焙选项、图集(atlas)存储布局、GPU 烘焙管线、运行时采样原理与多房间部署实战,读完即可在自己的场景中接入并调优这套探针光照方案。
一、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 参数可省略;从源码 constructor(examples/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。共享的 CubeRenderTarget 与 CubeCamera 在模块级按 (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.z、PADDING = 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+3 … 2*nz+2 : sub-volume 1 data
…
图集总深度 = 7 * ( nz + 2 )。源码实现中用 ATLAS_PADDING = 1 常量与公式 paddedSlices = nz + 2 * ATLAS_PADDING、atlasDepth = 7 * paddedSlices 创建 WebGL3DRenderTarget / RenderTarget3D(examples/jsm/lighting/LightProbeGridWebGL.js),并在 repack 阶段分别写出每个子体积的 leading padding(数据切片 0 的拷贝)、全部数据切片与 trailing padding(数据切片 nz-1 的拷贝)。烘焙产生的单个纹理即 .texture 指向的对象。
八、运行时采样与光照集成
烘焙完成后,把 grid 加入场景即可自动影响光照,无需手动采样——渲染器/光照系统在每个片元上按世界位置对图集做三维插值,按法线评估 L2 辐照度。
WebGPU:LightProbeGridNode
examples/jsm/tsl/lighting/LightProbeGridNode.js 中的 LightProbeGridNode 继承自 AnalyticLightNode。setup() 中(对应源码 L94-L132)完成三件事:
- 由
boundingBox计算采样坐标:将世界位置沿法线偏移半个探针间距(缓解法线自交导致的表面"贴脸"采样误差),再映射到图集的 texel 中心坐标,即uvw = (samplePos - min) / range的 clamp 与缩放; evaluateGridIrradiance()按 Z 定位到 7 个子体积各自的起始 slice,用getShIrradianceAt()解出 L2 辐照度并 clamp 非负;- 结果乘
.intensity后builder.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.js 及 src/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)演示了两个房间各放一个独立体积的做法,其关键经验可总结为:
- 重新烘焙前先从场景移除旧网格(
scene.remove+dispose()),防止旧图集作为光源影响新烘焙(反馈); - 分别烘焙两个体积——左右各
new LightProbeGridWebGL( 7.8, 4.7, 7.6, resolution, resolution, resolution ),中心定在各房间中央,bake使用{ cubemapSize: 32, near: 0.05, far: 20 }; - 烘焙完成后再把网格加入场景(注意 bounces 场景则需先 add 再 bake,见第六节);
- 更新调试可视化:已有 helper 时重新绑定
helper.probes = 新网格并调用update(); - 示例 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.html、examples/webgl_lightprobes_complex.html 与 examples/webgpu_lightprobes_complex.html,并对照 examples/jsm/lighting/LightProbeGridWebGL.js 与 examples/jsm/tsl/lighting/LightProbeGridNode.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

