首页
/ three.js LightProbeGenerator 深度解析:从立方体环境贴图生成光照探针(Light Probe)

three.js LightProbeGenerator 深度解析:从立方体环境贴图生成光照探针(Light Probe)

2026-09-07 18:18:36作者:裘晴惠Vivianne

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.jsexport * 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.htmlwebgpu_lightprobe_cubecamera.html

静态方法详解

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

从指定的 radiance 环境贴图创建光照探针,要求环境贴图以立方体渲染目标(cube render target)表示:

static async fromCubeRenderTarget( renderer, cubeRenderTarget ) {
    // ...
    return new LightProbe( sh );
}

参数约定:

  • rendererWebGPURenderer | WebGLRenderer。源码中通过 renderer.coordinateSystem === WebGLCoordinateSystem ? -1 : 1源码)计算坐标翻转因子,并通过 renderer.isWebGLRenderer 区分两个后端的像素读取路径,因此两个渲染器都受支持。
  • cubeRenderTarget:环境贴图。该立方体渲染目标的纹理必须为 RGBA 格式,即需保证 cubeRenderTarget.texture.formatRGBAFormat——这是为了让内部的 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 等)。方法内部通过临时 canvas2d 上下文 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()LinearSRGBColorSpaceNoColorSpace 则原样通过;遇到其余色彩空间会输出 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) * pixelSizerow = 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 );

是整球立体角,除以 totalWeight 得到平均化因子。最终产出的 LightProbesh 编码光照方向分布信息,其 intensity 默认值为 1(可参考 LightProbe 构造函数)。

坐标系与后端差异(WebGL vs WebGPU)

源码 可以看出一个容易忽略的坑:立方体贴图的面序/轴向约定与渲染后端相关。方法开头的 flip 因子即用于此:

const flip = renderer.coordinateSystem === WebGLCoordinateSystem ? - 1 : 1;

随后映射 coord 时按 flip 翻转向量(例如 WebGL 后端 case 0coord.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 同时调节 lightProbeIntensitydirectionalLightIntensityenvMapIntensity,可以直观对比三种光照来源(见 webgl_lightprobe.html)。

该示例对应 WebGPU 版本为 webgpu_lightprobe.htmlfromCubeTexture 的调用方式完全一致——因为它不依赖渲染器。

实战二:运行时用 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 的差异点:

适用前提与限制(可从 WebGLCubeRenderTarget 源码 确认):

  • 渲染目标纹理格式需为 RGBA(默认即如此);fromCubeRenderTarget 会依据 texture.typeFloatType / 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/异步投影器,对外收敛为两个方法:

  1. 贴图已离线就绪、无需回读 GPU → 用 fromCubeTexture(同步、零 GPU 依赖),适合静态场景、固定 HDR 环境;
  2. 需要烘焙当前实时场景/动态切换环境 → 用 fromCubeRenderTarget(异步、跨 WebGL/WebGPU),配合 CubeCamera 每帧/按需更新,代价是一次像素回读;
  3. 无论哪种路径,都要求输入为 radiance 立方体贴图RGBA 格式正方形尺寸,且 fromCubeRenderTarget 必须传入与渲染管线一致的那个 renderer 实例以保证坐标系正确。

生成后的 LightProbe 本质是辐照度环境贴图的球谐等价物,适合为 PBR 材质补充全局漫反射间接光,尤其适合烘焙探针后离线复用、或通过 WebXR 拿到外部光照估计数据做真实感 AR。想深入探索时,可以从三处继续阅读仓库源码:LightProbeGenerator 投影实现LightProbe 光源对象、以及 SphericalHarmonics3 数学封装

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