three.js LightProbeGenerator 深度解析:从立方体环境贴图生成光照探针(Light Probe)
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
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 数学封装。
【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考