一、引言:实时 GI 的圣杯之路
实时全局光照是计算机图形学的终极问题之一。一句话区分两个概念:
- 直接光照(Direct Illumination):光源 → 着色点。计算简单,方案成熟(Shadow Map + Phong/PBR)。
- 间接光照(Indirect Illumination):光源 → 场景 → 着色点。计算昂贵,是 GI 真正的难点。
下面四张图是 1 / 2 / 16 次反弹的对比,能直观感受到间接光对真实感的贡献:




1.1 既有方案的局限
| 方案 | 光线传输 | 光照计算 | 动态光源 | 动态物体 | 备注 |
|---|---|---|---|---|---|
| Ray Tracing | 运行时 | 运行时 | ✅ | ✅ | 性能开销巨大 |
| Lightmap | 离线 | 离线 | ❌ | ❌ | 烘焙后即固化 |
| PRTGI | 离线 | 运行时 | ✅ | 部分支持 | 动态物体只接收不贡献 GI |
Lightmap 把所有信息都烘进了一张贴图,运行时一次纹理采样就拿到结果,性能极佳——代价是光源和场景都必须保持静态。一旦场景需要日月轮替、可移动光源,Lightmap 就力不从心了。
PRTGI 的巧思在于把渲染方程的两个阶段拆开:耗时的”光线与场景求交”留在离线,廉价的”光照计算”留在运行时——这样动态光源就能参与 GI 计算。
二、理论基础:从渲染方程到 PRT
2.1 漫反射的渲染方程
漫反射表面的辐照度(Irradiance)可写为对入射 radiance 在半球上的积分:
其中
光线追踪求解这个积分时干两件事:
- 光线与场景求交:从着色点向半球随机采样若干方向,每条光线和场景求交得到 hit point。这一步是性能瓶颈。
- 光照计算(Relight):把每个 hit point 的属性(法线、位置、Albedo)代入光照模型算 radiance。这一步开销很小。
2.2 妥协的艺术:只送大脑
“Send Cerebra Only.” —— 刘慈欣《三体》
实时领域充满妥协。既然 N 次反弹算不过来,那就只算一次反弹。从着色点 A 出发的光线打到的 B 点集合作为间接光源,B 点本身只接受直接光照:

数学上:
翻译成工程语言:
- 计算 A 点的直接光照
- 从 A 发射若干方向,与场景求交得到点集
- 对每个 B ∈ Ω,计算其直接光照(带阴影)
- 加权求和
这个流程的关键洞察是:只要预先知道每个 B 点的法线、世界坐标和反照率,运行时就能用任意光源信息把这些点的直接光照算出来。这就是 “Surfel Cache”——把场景的几何属性记录成一团表面元素(surface elements)。
2.3 PRT 的核心拆分
| 阶段 | 干什么 | 数据 |
|---|---|---|
| 离线烘焙 | 在场景中均匀撒探针,每个探针用 CubeMap 捕获周围的 Albedo / Normal / WorldPos | Surfel Cache |
| 离线投影 | 把每个探针的方向相关数据投影到 SH 基函数 | SH 系数(每个探针 9 维 × N 通道) |
| 运行时 Relight | 对当前光照条件,重建每个 surfel 的直接光照,再投影回 SH | 动态 SH 系数 |
| 运行时着色 | 着色点采样附近探针的 SH,与法线方向点乘得到 irradiance | 最终 GI |
接下来的核心问题是:怎么用尽可能少的数据描述每个探针周围的方向相关信息?答案是 SH。
三、球谐函数:低频环境光的高效压缩
3.1 为什么是 SH
球谐函数(Spherical Harmonics)是定义在球面上的一组正交基函数。任何球面函数
其中
SH 之所以适合 GI,理由有三:
- 漫反射 BRDF 是低频的。Ramamoorthi & Hanrahan 在 2001 年证明:用 L=2(前 9 项)SH 重建的 irradiance,平均误差 < 1%。
- 正交性带来积分简化。两个 SH 函数的卷积变成系数点积:
。运行时着色时这点尤其重要。 - 旋转不变性。SH 系数在旋转下的变换是线性的,可以通过 Wigner 矩阵高效完成。
3.2 实数 SH 基函数(L ≤ 2)
工程上用得最多的是 L=2 的 9 个实数 SH 基函数:
| 索引 | 函数表达式 | 含义 | |
|---|---|---|---|
| 0 | (0, 0) | 常数项(环境光) | |
| 1 | (1,-1) | y 轴方向梯度 | |
| 2 | (1, 0) | z 轴方向梯度 | |
| 3 | (1, 1) | x 轴方向梯度 | |
| 4 | (2,-2) | 二阶混合项 | |
| 5 | (2,-1) | 二阶混合项 | |
| 6 | (2, 0) | 沿 z 的二阶项 | |
| 7 | (2, 1) | 二阶混合项 | |
| 8 | (2, 2) | 二阶各向异性 |
下面这个交互式可视化把 9 个基函数都画出来——用鼠标拖拽可以旋转视角,正值显示为暖色(红),负值显示为冷色(蓝):
3.3 投影与重建
把一个 cubemap 上的 radiance 投影到 SH,本质就是离散化的内积求和:
其中
更妙的是,漫反射卷积可以在 SH 系数空间直接完成。Ramamoorthi 给出了 cosine lobe 的 SH 系数(
| 阶 | |
|---|---|
| 0 | |
| 1 | |
| 2 |
这意味着运行时着色就是 9 次 MAD(multiply-add):几乎免费。
四、离线预计算阶段
4.1 探针布置策略(Probe Placement)
最直观的方式是均匀网格(Uniform Grid),但实际工程中纯粹的均匀网格非常浪费——墙体内部、天花板上方的探针完全用不到。常见策略:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Uniform Grid | 实现简单,运行时插值快 | 内存浪费严重 | 室内小场景、demo |
| NavMesh-driven | 只在可达区域撒探针 | 需要导航网格数据 | 室内 / 关卡式场景 |
| Voxelization-driven | 用场景体素化结果剔除内部探针 | 需要离线体素管线 | 大世界、地形 |
| 自适应密度 | 在几何复杂处加密 | 运行时需要 KD-Tree / BVH 查询 | 离线渲染或下一代引擎 |
工程上一种实用做法是:先撒均匀网格,再用 raycast 剔除”被几何包裹”的无效探针,最后给探针打有效性标志位。运行时着色时如果命中无效探针就 fallback 到附近的有效探针。
4.2 探针数据采集(Surfel Cache)
每个探针位置渲染一张低分辨率 CubeMap(典型 32×32×6 或 64×64×6),但记录的不是 radiance,而是 G-Buffer 通道:
- WorldPos:用于运行时计算阴影、距离衰减
- WorldNormal:用于光照计算
- Albedo:表面反照率(可能含 emission)
- (可选)Depth:用于 Chebyshev 漏光防御
存储形式上,把 6 个 cubemap face 展开成一张 octahedral encoded 的 2D 贴图(octahedron mapping)能让运行时采样更快,且省掉 cubemap 的边缘处理。
1 | |
4.3 SH 投影的 Compute Shader 实现
烘焙时给每个探针起一个 dispatch,把 surfel cache 投影到 SH。关键是要把”光照”和”几何”分开存——光照部分(直接光的 radiance)会在运行时重建,所以这里只投影几何(其实就是把每个 surfel 的方向作为基函数采样点,把 surfel 索引按方向打包)。
不同流派做法不同。一种典型实现:
1 | |
4.4 数据打包与存储
L=2 SH 每通道 9 个系数,RGB 三通道就是 27 个 float。每个探针 27 floats = 108 bytes。
- Texture3D 打包:天然支持硬件三线性插值。把 9 个 SH 系数拆成 3 张 RGBA Texture3D(前 3 张存 R/G/B 各 4 个系数,第 4 张补齐)。运行时单个
SAMPLE_TEXTURE3D自动完成 8 个角点的三线性混合。 - StructuredBuffer 打包:灵活,但插值要手写。适合非均匀探针布局。
工业界 Unity 的 APV(Adaptive Probe Volume)和 UE 的 LPV 都是后者的变体。一个可行的 Texture3D 打包格式:
| 通道布局 | 存储内容 |
|---|---|
| Tex3D_0 RGBA | (R0, R1, R2, R3) |
| Tex3D_1 RGBA | (R4, R5, R6, R7) |
| Tex3D_2 RGBA | (R8, G0, G1, G2) |
| Tex3D_3 RGBA | (G3, G4, G5, G6) |
| Tex3D_4 RGBA | (G7, G8, B0, B1) |
| Tex3D_5 RGBA | (B2, B3, B4, B5) |
| Tex3D_6 RGB | (B6, B7, B8) |
7 张 Texture3D、108 bytes/probe,对一个 64×16×64 的探针体积就是约 7 MB——完全可接受。
五、实时重光照阶段(Relight Pass)
5.1 着色点 → 探针的插值
着色点 P 落在某个探针格子里,需要从 8 个角点探针拿数据加权混合。两种主流插值方式:
三线性插值(Trilinear)
最简单,也是 Texture3D 硬件天然支持的。8 个角点权重为:
下面这个交互可视化:拖拽中间的红色着色点,能看到 8 个角点探针的权重实时变化(球大小代表权重):
四面体插值(Tetrahedral)
适合非规则探针布局。把空间用 Delaunay 四面体化,着色点落在某个四面体内,用四面体的重心坐标做权重插值(4 个探针)。Unity Light Probe Group 用的就是这套。优点是探针布置自由,缺点是查询开销大(需要预构建四面体邻接表)。
5.2 重建直接光照
拿到当前光源的方向、强度和阴影信息后,对每个探针的 surfel 重新计算直接光:
1 | |
这一步通常在 Compute Shader 里跑,每帧或每隔几帧更新一次(大世界场景可以做时间分片,每帧只更新一部分探针)。
5.3 着色点的最终采样
1 | |
5.4 时域累加:模拟多次反弹
只算一次反弹会让画面显得”浅”。一个聪明的做法:把上一帧 Relight 的结果作为这一帧 surfel 的 emission 输入。
1 | |
这样数学上等价于一个无穷级数:第 N 帧的探针值 ≈ 1 次反弹 + 0.9·(2 次反弹) + 0.9²·(3 次反弹) + …
实测下来 5–10 帧之内就收敛到接近多次反弹的视觉效果。这是 PRTGI 看起来”亮”的关键技巧。需要注意能量守恒——衰减系数(这里的 0.9)应该由材质平均反照率推导,避免能量爆炸。
六、工程痛点与解决方案
6.1 漏光与漏影(Light / Shadow Leaking)
这是 PRTGI 最讨厌的伪影。问题根源在于:插值权重只考虑了空间距离,不考虑几何遮挡。

典型场景:薄墙两侧,墙内一侧的探针被点亮,光照通过插值”穿透”到墙外暗面。
解法 1:法线权重剔除(Normal Weight)
最简单,但效果有限。在权重里乘上
解法 2:Chebyshev 可见性测试
借鉴 VSM(Variance Shadow Map)的思路。每个探针不只存 SH 系数,还存一张距离场:从探针中心到周围最近表面的平均距离
下面这个交互可视化展示了 Chebyshev 在 1D 情况下的可见性曲线(拖动滑块改变 μ 和 σ²,看 P(visible) 怎么变化):
直觉解释:
- 当
,说明着色点离探针比探针看到的最近表面还近 → 没有遮挡 → 可见 - 当
,”被遮挡”的概率随 增长,但 (深度方差)大时表面起伏大,依然可能可见
实际工程中通常会再加一个 light bleeding reduction:p = saturate((p - 0.2) / 0.8),把弱可见性直接 clamp 到 0,进一步抑制漏光。
解法 3:探针有效性(Validity)+ 重投影
The Division 的方案:烘焙时给每个探针打 validity 标记(探针在墙体内 → invalid),运行时着色点会跳过 invalid 探针并重新分配权重。
6.2 性能开销与时间分片
一个 64×16×64 = 65536 个探针的体积,每帧全量 Relight 不现实。常见优化:
| 优化 | 思路 | 收益 |
|---|---|---|
| 时间分片更新 | 每帧只更新 1/N 的探针 | Relight 开销线性下降 |
| 距离 LOD | 远处探针用更低频率(甚至只用静态烘焙) | 大世界必备 |
| 视锥剔除 | 只更新视锥内的探针 | 第三人称游戏可省 50%+ |
| GPU Indirect Dispatch | 烘焙数据预处理时按 active probe 打包 | 省掉 cull 分支 |
| 半精度存储 | SH 系数用 fp16 | 显存减半,画质几乎无损 |
一个参考数字:在 Unity URP + RTX 3070 上,65k 探针、每帧更新 1/8 + 着色 2.5K × 1440p,整个 Relight Pass 约 0.4 ms,最终 GI Sample 约 0.3 ms。
6.3 动态物体的处理
PRTGI 的根本限制是动态物体不参与光线传输——它们能”被照亮”,但不会贡献 GI。常见的折中:
- 接收方向:动态物体在着色时直接采样 Probe Volume,自然吃到 GI
- 贡献方向:用一个轻量的运行时方案补充(比如屏幕空间的 SSGI、或者反射探针)
- 影子与遮挡:动态物体的接触阴影靠 Capsule Shadow / SSAO 兜底
6.4 内存布局陷阱
Texture3D 的内存布局是 swizzled 的,如果探针数据按 (x, y, z) 线性写入,Compute Shader 的访存模式会非常差。建议烘焙输出时就用 Morton 编码(Z-order curve)排列,运行时 Texture3D 采样会自动命中 cache。
七、效果对比与适用场景
虽然两张图的太阳光方向不一致,但仍然能明显看出 PRTGI相比传统 无GI 方案 在阴影处的暗部细节更丰富、过渡更柔和——这正是间接光在起作用。引用闫令琪教授的话:“评估 GI 好不好,看画面亮不亮、暗部细节明度对比丰不丰富”。
适用性总结
| 场景类型 | PRTGI 适用度 | 备注 |
|---|---|---|
| 室内静态关卡 | ⭐⭐⭐ | Lightmap 也行,PRTGI 优势在动态光 |
| 日夜循环开放世界 | ⭐⭐⭐⭐⭐ | PRTGI 几乎是最佳选择 |
| 大量动态物体 | ⭐⭐ | 需要配合 SSGI / 实时探针更新 |
| 实时场景破坏 | ⭐ | Surfel cache 离线烘焙的硬伤 |
| 程序化生成关卡 | ⭐ | 没法离线烘焙 |
与下一代技术的关系
PRTGI 的思路在 2024 年后依然有生命力,但被进一步改造:
- Unity APV (Adaptive Probe Volume):自适应密度的探针体积,本质是 PRTGI 的工程化版本
- UE5 Lumen:屏幕空间 + 距离场的 hybrid GI,可以看作 “运行时 surfel cache”
- RTXGI / DDGI:用硬件 RT 实时更新探针的 radiance,绕过了”只能一次反弹”的限制
如果你的项目硬件门槛要兼容到 RTX 2060 以下,PRTGI 仍然是性价比最高的方案。
Precomputed Radiance Transfer
PRT 原始论文 | “Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments” — Sloan et al., SIGGRAPH 2002
SH 辐照度环境映射 | “An Efficient Representation for Irradiance Environment Maps” — Ramamoorthi & Hanrahan, SIGGRAPH 2001
工业界实现
全境封锁 GI 实现 | “Global Illumination in Tom Clancy’s The Division” — Nikolay Stefanov, GDC 2016
Unity APV 文档 | Unity Docs > Adaptive Probe Volumes
社区文章
PRTGI 理论与实战 | 知乎 > 预计算辐照度全局光照(PRTGI)从理论到实战 — AKG4e3
实时 PRTGI 实现 | 知乎 > 实时 PRTGI 技术与实现 — 网易雷火
Light Leakage | 知乎 > 体探针漏光解决方案汇总
SH 球谐函数学习路径
Stupid SH Tricks | “Stupid Spherical Harmonics (SH) Tricks” — Peter-Pike Sloan (SH 工程实践圣经)
本站相关文章 | SH 球谐环境光照理论