three.js LightProbeGenerator 深度解析:从立方体环境贴图生成光照探针(Light Probe)
LightProbeGenerator 是 three.js 提供的实用工具类,用于把以立方体环境贴图(cube map)形式存在的 radiance 环境光照数据,重编码为可直接放入场景的三阶球谐(SH)光照探针 LightProbe。本文基于其官方 API 文档(LightProbeGenerator.html.md),结合 源码实现 与仓库内的四个官方示例,完整讲解两个静态方法的使用前提、参数细节、底层球谐投影算法与可运行的接入代码。
阅读完本文后,你将掌握:如何用一张 HDR/LDR 立方体贴图在数毫秒内生成漫反射光照探针(fromCubeTexture),如何在运行时用 CubeCamera 实时捕捉场景并异步生成动态探针(fromCubeRenderTarget),以及 WebGL / WebGPU 两种渲染后端下的差异与注意事项。
LightProbeGenerator 是什么:光照探针的“编码器”
在 three.js 中,LightProbe 是一类特殊光源:它不直接发光,而是把“穿过三维空间的光照信息”预先编码起来,渲染时用探针数据近似计算打到物体上的光。正如 LightProbe 官方文档 所述,three.js 当前实现的是漫反射光照探针(diffuse light probe),其功能等价于一张辐照度环境贴图(irradiance environment map)。
LightProbeGenerator 正是 LightProbe 的“编码器”:输入是一张已包含场景辐射亮度(radiance)的立方体环境贴图,输出是一个可直接 scene.add() 的 LightProbe 实例。它对外仅暴露两个静态方法,分别处理两种常见的数据载体:
| 静态方法 | 输入载体 | 返回 |
|---|---|---|
fromCubeTexture( cubeTexture ) |
已加载的 CubeTexture(离线贴图) |
LightProbe(同步) |
fromCubeRenderTarget( renderer, cubeRenderTarget ) |
CubeRenderTarget / WebGLCubeRenderTarget(离线或运行时渲染的渲染目标) |
Promise.<LightProbe>(异步) |
它属于 examples 下的 addon(附加组件),而非 three.js 核心构建产物。除 源码本身 外,它也被聚合导出到 examples/jsm/Addons.js(export * from './lights/LightProbeGenerator.js'),因此可通过统一的 three/addons/ 命名空间导入。
Import:addon 必须显式导入
由于 LightProbeGenerator 不在核心模块中,使用前必须像其他 addon 一样显式导入:
import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js';
仓库内的四个官方示例均使用这一写法,例如 webgl_lightprobe.html(WebGL)、webgpu_lightprobe.html(WebGPU)、webgl_lightprobe_cubecamera.html 与 webgpu_lightprobe_cubecamera.html。
静态方法详解
.fromCubeRenderTarget( renderer, cubeRenderTarget ) : Promise.(异步)
从指定的 radiance 环境贴图创建光照探针,要求环境贴图以立方体渲染目标(cube render target)表示:
static async fromCubeRenderTarget( renderer, cubeRenderTarget ) {
// ...
return new LightProbe( sh );
}
参数约定:
- renderer:
WebGPURenderer | WebGLRenderer。源码中通过renderer.coordinateSystem === WebGLCoordinateSystem ? -1 : 1(源码)计算坐标翻转因子,并通过renderer.isWebGLRenderer区分两个后端的像素读取路径,因此两个渲染器都受支持。 - cubeRenderTarget:环境贴图。该立方体渲染目标的纹理必须为 RGBA 格式,即需保证
cubeRenderTarget.texture.format为RGBAFormat——这是为了让内部的readRenderTargetPixels类像素回读能正常工作(源码注释亦明确说明,见 源码)。
返回值:一个 Promise,resolve 后得到创建好的 LightProbe。
纹理数据类型适配:方法读取 cubeRenderTarget.texture.type,并针对三种类型分别解码像素(源码):
texture.type |
读取的数组类型 | 像素解码 |
|---|---|---|
FloatType |
Float32Array |
直接取 r/g/b 浮点值 |
HalfFloatType |
Uint16Array |
经 DataUtils.fromHalfFloat() 还原浮点 |
其他(默认视为 UnsignedByteType) |
Uint8Array |
除以 255 归一化到 [0,1] |
若当前为 UnsignedByteType(即 LDR 数据),内部会先做线性化处理。
像素回读:WebGL 后端走 renderer.readRenderTargetPixelsAsync( cubeRenderTarget, 0, 0, width, height, data, faceIndex ),WebGPU 后端则将第 6 个参数固定为 0、以第 7 个参数传入 faceIndex(源码)。这正是该方法需要 async 并返回 Promise 的原因——它依赖一次异步的 GPU→CPU 像素回读。
.fromCubeTexture( cubeTexture : CubeTexture ) : LightProbe
从指定的 radiance 立方体纹理创建光照探针,返回同步创建的 LightProbe:
static fromCubeTexture( cubeTexture ) {
// ...
return new LightProbe( sh );
}
- cubeTexture:环境贴图。此时像素数据已经存在于 CPU 侧的
cubeTexture.image[faceIndex](每面一张ImageBitmap/HTMLImageElement等)。方法内部通过临时canvas与2d上下文drawImage/getImageData取出每面像素(源码),因此它本质上是 CPU 同步计算,适合离线烘焙、贴图加载完成后一次性生成探针的场景。
两条方法在核心算法上完全一致(下面详述),差异仅在于“如何拿到 6 个面的原始像素”:一个来自渲染目标回读,一个来自贴图解码。
底层原理:把立方体贴图“投影”为三阶球谐系数
两个方法最终都执行同一套 SH 投影流水线。理解它能帮你判断何时可用、结果精度如何。代码开头注释引用了 Peter-Pike Sloan 关于 SH 编码的经典讲义(StupidSH36,见 源码),核心流程如下:
① 对 6 个面逐像素遍历,每个像素按 RGBA 取色并线性化:
color.setRGB( data[ i ] / 255, data[ i + 1 ] / 255, data[ i + 2 ] / 255 );
convertColorToLinear( color, cubeTexture.colorSpace );
线性化由模块内私有函数 convertColorToLinear 完成(源码):当色彩空间为 SRGBColorSpace 时调用 convertSRGBToLinear();LinearSRGBColorSpace 与 NoColorSpace 则原样通过;遇到其余色彩空间会输出 console.warn 警告。这意味着:LDR 的 sRGB 立方体贴图会被正确线性化后再参与球谐累加,避免探针偏暗。
② 把像素坐标映射为单位立方体上的方向,并计算像素权重。球谐在球面上做积分,因此每个像素必须按其“立体角”加权。源码用透视投影近似立体角权重(源码):
const lengthSq = coord.lengthSq();
const weight = 4 / ( Math.sqrt( lengthSq ) * lengthSq );
totalWeight += weight;
dir.copy( coord ).normalize();
其中 coord 由像素的行列索引经 col = -1 + (pixelIndex % imageWidth + 0.5) * pixelSize、row = 1 - (floor(pixelIndex / imageWidth) + 0.5) * pixelSize 得到,再按 6 个面各自的朝向(case 0..5)摆放到单位立方体表面。立方体贴图纹理本身被假定为正方形(imageWidth 取自单边宽高,源码注释 “assumed to be square”,见 源码)。
③ 在方向 dir 上求值三阶 SH 基函数,并加权累加 9 个系数:
SphericalHarmonics3.getBasisAt( dir, shBasis );
for ( let j = 0; j < 9; j ++ ) {
shCoefficients[ j ].x += shBasis[ j ] * color.r * weight;
shCoefficients[ j ].y += shBasis[ j ] * color.g * weight;
shCoefficients[ j ].z += shBasis[ j ] * color.b * weight;
}
三阶球谐共 9 个系数(band 0~2),分别存于 SphericalHarmonics3.coefficients 的 9 个 Vector3 中。基础方向求值由三阶 SH 的解析公式给出(getBasisAt,可参考 SphericalHarmonics3),因此整个投影只需做像素遍历与累加,无需解线性方程组。
④ 归一化并返回 LightProbe:
const norm = ( 4 * Math.PI ) / totalWeight;
// 对全部 9 个系数乘以 norm ...
return new LightProbe( sh );
4π 是整球立体角,除以 totalWeight 得到平均化因子。最终产出的 LightProbe 以 sh 编码光照方向分布信息,其 intensity 默认值为 1(可参考 LightProbe 构造函数)。
坐标系与后端差异(WebGL vs WebGPU)
从 源码 可以看出一个容易忽略的坑:立方体贴图的面序/轴向约定与渲染后端相关。方法开头的 flip 因子即用于此:
const flip = renderer.coordinateSystem === WebGLCoordinateSystem ? - 1 : 1;
随后映射 coord 时按 flip 翻转向量(例如 WebGL 后端 case 0 用 coord.set( -1 * flip, row, col * flip ),见 源码)。因此在接入自定义渲染管线时,务必保证传给 fromCubeRenderTarget 的 renderer 与你实际使用的渲染器是同一个实例,否则方向翻转错误会导致探针光照“左右/前后颠倒”的诡异效果。
实战一:用 CubeTexture 生成静态探针(fromCubeTexture)
官方示例 webgl_lightprobe.html 展示了最典型的离线用法:加载一张多面 Pisa 立方体贴图 → 生成探针 → 用标准材质观察光照。核心片段如下:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js';
import { LightProbeHelper } from 'three/addons/helpers/LightProbeHelper.js';
// 准备场景、相机、渲染器(此处省略)
lightProbe = new THREE.LightProbe();
scene.add( lightProbe );
// 构造 6 面 URL(px/nx/py/ny/pz/nz)
const urls = genCubeUrls( 'textures/cube/pisa/', '.png' );
new THREE.CubeTextureLoader().load( urls, function ( cubeTexture ) {
scene.background = cubeTexture; // 立方体贴图同时作为背景
lightProbe.copy( LightProbeGenerator.fromCubeTexture( cubeTexture ) );
lightProbe.intensity = 1.0;
// 注意:lightProbe.position 不参与场景光照计算(仅 LightProbeHelper 跟随其位置)
const material = new THREE.MeshStandardMaterial( {
color: 0xffffff,
metalness: 0,
roughness: 0,
envMap: cubeTexture, // 材质仍可使用原贴图做高光反射
envMapIntensity: 1
} );
scene.add( new THREE.Mesh( new THREE.SphereGeometry( 5, 64, 32 ), material ) );
// 可视化探针:一个用 SH 系数实时着色的球体
scene.add( new LightProbeHelper( lightProbe, 1 ) );
renderer.render( scene, camera );
} );
关键点:
- 返回值直接
lightProbe.copy(...)即可(LightProbe.copy内部会复制sh系数,见 LightProbe.copy)。 - 探针只编码漫反射(低频)光照;镜面高光仍依赖
envMap。示例中材质同时设置了envMap与探针,正是“探针补漫反射、envMap 补反射”的经典组合。 - 用 GUI 同时调节
lightProbeIntensity、directionalLightIntensity与envMapIntensity,可以直观对比三种光照来源(见 webgl_lightprobe.html)。
该示例对应 WebGPU 版本为 webgpu_lightprobe.html,fromCubeTexture 的调用方式完全一致——因为它不依赖渲染器。
实战二:运行时用 CubeCamera 捕捉动态探针(fromCubeRenderTarget)
如果需要探针随场景内容变化(例如移动光源、动态物体、切换背景),可以用 CubeCamera 每帧/每段间隔把场景渲染进立方体渲染目标,再异步生成探针。官方示例 webgl_lightprobe_cubecamera.html 的做法:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { LightProbeGenerator } from 'three/addons/lights/LightProbeGenerator.js';
import { LightProbeHelper } from 'three/addons/helpers/LightProbeHelper.js';
// 1. 创建立方体渲染目标与 CubeCamera
const cubeRenderTarget = new THREE.WebGLCubeRenderTarget( 256 );
cubeCamera = new THREE.CubeCamera( 1, 1000, cubeRenderTarget );
// 2. 场景先准备一个空的 LightProbe
lightProbe = new THREE.LightProbe();
scene.add( lightProbe );
// 3. 立方体贴图加载完成后:先把场景渲染到 cube 渲染目标
new THREE.CubeTextureLoader().load( urls, async function ( cubeTexture ) {
scene.background = cubeTexture;
cubeCamera.update( renderer, scene ); // 将场景渲染进 cubeRenderTarget(6 个面)
// 4. 异步回读 6 个面的像素并生成探针
const probe = await LightProbeGenerator.fromCubeRenderTarget( renderer, cubeRenderTarget );
lightProbe.copy( probe );
scene.add( new LightProbeHelper( lightProbe, 5 ) );
renderer.render( scene, camera );
} );
WebGPU 版本 webgpu_lightprobe_cubecamera.html 的差异点:
- 渲染目标为 WebGPU 侧的
CubeRenderTarget(对应 src/renderers/common/CubeRenderTarget.js,二者都继承自RenderTarget并创建 6 面CubeTexture); - 调用
cubeCamera.update( renderer, scene )前需先await renderer.init()(见 webgpu_lightprobe_cubecamera.html),确保 GPU 上下文就绪。
适用前提与限制(可从 WebGLCubeRenderTarget 源码 确认):
- 渲染目标纹理格式需为 RGBA(默认即如此);
fromCubeRenderTarget会依据texture.type是FloatType/HalfFloatType/UnsignedByteType选择对应解码分支,推荐用半浮点以兼顾动态范围与带宽; - 渲染目标被假定为正方形(示例用 256×256);
- 该路径包含一次异步 GPU→CPU 回读(
readRenderTargetPixelsAsync),属于阻塞型操作,适合低频更新(环境明显变化时触发),不建议每帧在移动端执行。
让探针“看得见”:LightProbeHelper 可视化
调试时可通过 helper 直观检查探针编码是否正确。WebGL 侧用 examples/jsm/helpers/LightProbeHelper.js:
const helper = new LightProbeHelper( lightProbe, 5 ); // 第二个参数为球体大小
scene.add( helper );
其原理是用一个 ShaderMaterial 球体,在片元着色器中通过法线求值 9 个 SH 系数的辐照度(shGetIrradianceAt),再乘 intensity 输出为颜色(见 LightProbeHelper 着色器)。球体每个方向上的颜色即是该方向探针“感受到”的辐照度,帮助判断贴图方向、翻转与强度是否正确。该类文档注释指出其仅适用于 WebGLRenderer(WebGPU 需改用 LightProbeHelperGPU.js 变体),且辅助球体会跟随 lightProbe.position——但如前所述,探针位置本身不参与漫反射光照计算,只影响 helper 的摆放。
总结与选型建议
综合文档与源码,LightProbeGenerator 的本质是一段“cube map → 三阶球谐”的 CPU/异步投影器,对外收敛为两个方法:
- 贴图已离线就绪、无需回读 GPU → 用
fromCubeTexture(同步、零 GPU 依赖),适合静态场景、固定 HDR 环境; - 需要烘焙当前实时场景/动态切换环境 → 用
fromCubeRenderTarget(异步、跨 WebGL/WebGPU),配合CubeCamera每帧/按需更新,代价是一次像素回读; - 无论哪种路径,都要求输入为 radiance 立方体贴图、RGBA 格式、正方形尺寸,且
fromCubeRenderTarget必须传入与渲染管线一致的那个 renderer 实例以保证坐标系正确。
生成后的 LightProbe 本质是辐照度环境贴图的球谐等价物,适合为 PBR 材质补充全局漫反射间接光,尤其适合烘焙探针后离线复用、或通过 WebXR 拿到外部光照估计数据做真实感 AR。想深入探索时,可以从三处继续阅读仓库源码:LightProbeGenerator 投影实现、LightProbe 光源对象、以及 SphericalHarmonics3 数学封装。
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