约 个字 行代码 预计阅读时间 分钟
Dynamic Global Illumination and Lumen
Global Illumination (GI)
全局光照的目标就是要实现渲染方程(rendering equation)【Kajiya】指出的光照效果:
但到目前为止,还没有哪一种技术能够完全满足渲染方程的要求。因为渲染方程除了考虑直接光照,还考虑来自四面八方的光照(间接光照),并且还要和不同材质的 BRDF 进行复杂的积分计算。这对计算机而言计算量实在太大了,并且在游戏中还得保证 1s 内渲染几十帧,想想看就知道是一个难题。
右图就是 CG 领域的一个知名案例康奈尔盒(Cornell Box)。可以看到位于天花板中央的面光源,并且左右两面分别是红色和绿色的墙。它们的反光打在中间的两个几何体上,呈现出相应的颜色。
例子
比较关闭/开启全局光照的效果,不难看出全局光照在游戏中相当重要。
Monte Carlo Integration
既然积分的解析解计算很复杂,那么该怎么解呢?一种常用算法是蒙特卡洛积分法(Monte Carlo integration),取随机采样值的平均值作为积分的近似结果。
例子
可以看到,由于采样数有限,所以渲染结果中存在较多的噪点。
Sampling
所以采样就成为了关键。随着每个像素的样本数量(SPP)增加,噪点逐渐减少。左上角显示的是每像素 1 个样本,从左到右每个方格的样本数依次翻倍。
最简单的采样方式是均匀采样(uniform sampling)。但很显然,假如信号分布不均匀,如果采样率不高,均匀采样可能就会漏掉分布集中的信号。
积分计算公式为:
其中红色部分是概率密度函数(probability density function, PDF),它描述了这个随机变量取特定值的相对可能性,数值越高表示被选中的可能性越大。这给我们的启示是:如果采样也按照这样的概率分布进行的话,那么少量采样点就有可能得到不错的近似结果。一般形式的公式如下:
既然 PDF 可以是任意的,那么什么样的 PDF 能得到最佳采样效果呢?我们先将渲染方程表示成用蒙特卡洛积分法近似后的形式:
其中 \(L_{i}\left(p, \omega_{i}\right) f_{r}\left(p, \omega_{i}, \omega_{o}\right)\left(n \cdot \omega_{i}\right)\) 是被积函数,PDF 就是 \(p(\omega_i)\)。
- 均匀采样:\(p(\omega_i) = \dfrac{1}{2\pi}\)
-
余弦加权(cosine-weighted):\(p(\omega_i) = \dfrac{\cos \theta}{\pi}\)
显然余弦加权取得更好的效果,因此采样点的分布会根据球形特征变化,比如球顶部分采样点会更密集些
-
GGX:\(p(\omega_i) = \dfrac{\alpha^2 \cos \theta}{\pi((\alpha^2 - 1) \cos^2 \theta + 1)^2}\)
GGX 更适合表现高光效果
Reflective Shadow Maps (RSM)
然而,基于蒙特卡洛积分的光线追踪只能进行离线渲染,难以满足实时渲染的需求。于是有人发明了一种叫做反射阴影贴图(reflective shadow map, RSM)的方法,它要做的是将光「注入」到场景中。整体思路如下:
注:这和另一种渲染算法光子映射(photon mapping)很像。
-
阴影贴图上的每个像素都是一个间接光源
-
RSM 像素 \(X_p\) 照亮位置 \(x\) 的程度:
\[ E_{p}(x, n)=\phi_{p} \frac{\max \left\{0,\left\langle n_{p} \mid x-x_{p}\right\rangle\right\} \max \left\{0,\left\langle n \mid x_{p}-x\right\rangle\right\}}{\left\|x-x_{p}\right\|^{4}} \] -
表面点 \(x\) 处的间接辐照度可以通过累加所有像素光源的照明效果来近似计算(不考虑遮挡)
\[ E(x, n) = \sum_{\text{pixel } p} E_p(x, n) \]
-
锥体追踪(cone tracing):收集间接光照
- 随机采样 RSM 像素
- 预先计算这样的采样模式,并在所有间接光照计算中重复使用
- 400 个采样点足够多了
- 使用泊松采样以获得更均匀的样本分布(后来人们发现了更好的采样方法)
-
使用低分辨率(low-res)的间接光照加速
-
对于完整分辨率图像中的每个像素:
- 获取其周围的四个低分辨率的采样点
- 通过比较法线和世界空间位置进行验证
- 双线性插值
-
下图的红色像素表示对应采样点的法线差异过大的像素点,需要进行一次完整的采样
-
例子
一些新颖的思路
- 容易实现
- 使用 RSM 进行光子注射
- mipmap 中的锥体采样
- 带有误差检查的低分辨率间接光照
缺点
- 单次弹射
- 不对间接光照进行可见性检查
Light Propagation Volumes (LPV)
光线传播体积(light propagation volumes, LPV)技术最早引入于 CryEngine 3(2009)。
其核心思路是:使用 3D 网格将辐射从直接照亮的表面传播到其他任何地方。
执行步骤如下(其中最关键的是第三步):
- 辐射点集场景表示的生成
-
将虚拟光源点云注入到辐射体积中
- 将场景预先细分为 3D 网格
- 针对每个网格单元,定位其内部包含的虚拟光源
- 汇总这些光源的方向性辐射分布
- 投影至前两阶球谐函数(SH)(共 4 个系数)
-
体积辐射传播(volumetric radiance propagation)
- 对于每个网格单元,收集其六个面接收到的辐射度
- 进行求和,并再次使用 SH 表示
- 重复此传播过程数次,直至体积达到稳定状态
-
采用最终的光照传播体积进行场景照明
光的传播速度和范围和迭代次数有关:
注:该方法在物理学层面可能有不太严谨的地方。
Sparse Voxel Octree for Real-time Global Illumination (SVOGI)
用于实时全局光照稀疏体素八叉树(sparse voxel octree for real-time global illumination, SVOGI)由 NVIDIA 某团队发明。以下是体素化的过程:
- 保守光栅化(conservative rasterization):保证再薄再小的三角形也能被体素化
收集表面体素:
接下来的计算过程:
- 将辐照度(irradiance)注入体素中的光源
- 过滤八叉树内的辐照度
在八叉树内用锥体追踪来着色(对于来自相机的第 2 趟):
- 基于漫反射 + 镜面 BRDF 发射若干锥体
- 根据锥体的(增长)尺寸在八叉树中进行查询
问题
数据结构过于复杂,难以在 GPU 上表达。
Voxelization Based Global Illumination (VXGI)

基于体素化的全局光照(voxelization based global illumination, VXGI)的特点:
-
将体素数据存储在裁剪图中
- 多分辨率纹理
- 靠近中心的区域具有更高的空间分辨率
- 似乎自然地适用于锥体追踪需求
-
构建裁剪图比 SVO 更简单
- 无需节点、指针等,由硬件处理
- 从剪辑映射读取更为便捷
-
裁剪图尺寸为 (64~256)3,包含 3~5 级细节层次
- 每个体素占用 16~32 字节 => 需要 12 MB~2.5 GB的显存
由于相机可能随时会动,所以体素会不断更新。为应对这一问题,我们采用环形寻址方法。
- 空间中的固定点始终映射到裁剪图中的同一地址
- 背景显示纹理地址:
frac(worldPos.xy / clipmapSize.xy)
例子
每个体素都是有一个不透明度的,所以光有可能会透过体素穿出。
- 存在一个三角形与一个体素
- 选择能产生最大投影面积的投影平面
- 使用 MSAA 技术对三角形进行光栅化,为每个像素计算覆盖掩码
- 获取 MSAA 采样点并将其重投影至其他平面
- 对所有被覆盖的采样点重复此过程
- 通过模糊所有重投影采样点来增厚处理结果
不透明度 = 覆盖的 MSAA 采样数 / MSAA 分辨率2
直接覆盖的结果
光线注入
- 计算包含被直接光照亮的表面的体素的辐射度
- 从 RSM 中获取信息
例子
使用锥体追踪来着色(基于 BRDF 生成不同锥体):
沿路径累积体素辐射度与不透明度:
例子
问题
- 不正确的遮挡(不透明度):简单结合 alpha 混合 + 不透明度
- 漏光(light leaking):当遮挡墙远小于体素大小时
Screen Space Global Illumination (SSGI)
寒霜(Frostbite)引擎于 2015 年提出了屏幕空间全局光照(screen space global illuminatino, SSGI)技术。它的大致思路是复用屏幕空间上的数据。比如对于下图,由于地面潮湿,看起来像镜面一样,所以红框标出的建筑在地面上有了倒影(白框标出),这些倒影可直接利用红框标出的实际内容得到,无需重新创建。
屏幕空间的辐射度采样(对于每个片元):
- 计算很多反射光线
- 沿光线方向行进(在深度 gbuffer 中)
- 使用命中点的颜色作为间接光照
最简单的光线行进算法是线性光线行进:
- 一般步骤:
- 以固定步长前进
- 每一步检查深度值
- 特点:
- 快速
- 但可能会跳过较薄的物体
但更好的方法是采用层级追踪(hierarchical tracing):
-
生成最小深度 mipmap
-
无栈光线遍历最小深度 mipmap
level = 0; while (level > -1) { stepCurrentCell(); if (above Z plane) level++; if (below Z plane) level--; }
例子
另一个有意思的思路是对临近像素的光线复用:
- 存储命中点数据
- 假设相邻节点间的可见性相同
- 将指向邻居命中点的光线视为有效
带有 mipmap 过滤的锥体追踪:
- 估算锥体在命中点的足迹(footprint),包括粗糙度以及到命中点的距离
- 对颜色 mipmap 采样,mip 层级由足迹决定
- 对颜色 mipmap(金字塔)预过滤
优点
- 快速实现光泽和镜面反射效果
- 质量优良
- 无遮挡问题
以下是 SSGI 的独特优势:
- 易于处理近距离接触阴影
- 精确命中点计算
- 与场景复杂度解耦
- 处理动态物体
缺点
- 屏幕外信息缺失
- 邻近光线复用的可见性错误的影响
以第一张图为例:立方体底部的倒影本该是黑色的,但由于屏幕空间中并没有黑色像素点,所以呈现出来的是白色。
Lumen
光线追踪的挑战(我们为什么需要 Lumen?)
-
速度慢
- 每个像素仅能负担 1/2 的光线
- 但是高质量 GI 需要数百条光线
-
采样难
-
辐照度场(irradiance fields)
- 漏光与过度遮挡
- 探针放置问题
- 光照更新缓慢
- 独特的平面化外观
-
屏幕空间去噪器(screen space denoiser)
- 许多复杂室内环境的渲染结果中噪点过多
- 但噪点并非一直存在
远离窗口后,采样点数量骤降,滤波器也救不了一点。
-
低分辨率过滤场景空间的探针照亮完整的像素
-
Phase 1: Fast Ray Trace in Any Hardware
第一个要面对的问题是在任何硬件设备上都能做到快速的光线追踪。为解决这一难关,Lumen 用到了符号距离场(signed distance field, SDF)。SDF 指的是每个点离最近表面的距离,并规定内部区域存储负距离(「符号」一词的来由)。不难想到,距离为 0 意味着该点就在表面上。
Mesh SDF
由于为整个场景存储 SDF 开销太大,因此只考虑为每个网格生成 SDF。
- 基于网格大小的分辨率
- Embree(Intel 开发的高性能光线追踪)点查询
- 追踪光线,并统计命中三角形背面的数量以确定符号(若超过 25% 的光线命中背面,则符号为负)
对于偏薄的网格,对其进行半体素扩张(half voxel expand)以解决漏光问题。不过由于引入了表面偏差,导致接触阴影丢失。尽管如此,过度遮挡总比漏光问题好一点。
有了 SDF 后,光线追踪就变得更加高效了:光线与表面相交时,可根据距离跳过空的区域。
- 安全且快速
- 位于点 p 处时,只需行进 SDF(p) 的距离
借助 SDF,锥体追踪的计算也变得高效,便于实现软阴影效果。
如果采样率很大,我们可以考虑用稀疏的网格 SDF,即将网格 SDF 划分成块,这有助于节省存储空间。
-
定义一个
max_encode_distance- 当任意的
sdf(brick) > max_encode_distance时无效
- 当任意的
-
IndirectionTable存储每个块的索引
我们还可以对网格 SDF 进行 LOD 操作
- GPU 收集每帧的请求
- CPU 下载请求,并流式调入/调出页面
- 生成 3 个 mip 层级
- 始终加载最低分辨率,其余两个进行流式传输
例子
Global SDF
虽然网格 SDF 能一定程度上减小开销,但是假如场景中存在很多物体,意味着存在大量网格,那么计算量还是特别大。
-
追踪相机射线并可视化步数
-
每条射线上的命中物体的数量
解决方案是采用全局 SDF。它能表达出不精确的近表面,因为仅在锥体开头对物体 SDFs 采样,剩余部分用的就是全局 SDFs。采用全局 SDF 能大幅降低重叠对象的追踪成本。
例子
右图便是用到全局 SDF 的效果:
我们可以缓存相机周围的全局 SDF。
- 围绕相机设置 4 个裁剪图
- 裁剪图随移动而滚动
- 远处裁剪图的更新频率较低
- 同时采用稀疏存储(节省约 16 倍的内存)
Phase 2: Radiance Injection and Caching
通过网格卡片(mesh card)技术,沿轴对齐方向(AABB)对物体的六个面拍摄六张快照,获取其光照信息。
例子
接下来根据卡片结果来生成表面数据的缓存,该过程分为两趟:
-
第一趟:卡片捕捉
- 固定每帧的纹素预算(512x512)
- 按与摄像机的距离及 GPU 的反馈顺序排序
- 捕获分辨率取决于卡片在屏幕上的投影(LOD),离相机近分辨率越高
Albedo:反射率。
-
表面缓存的大小为 4096x4096,里面有多个 128x128 大小的物理页,在里面把每一个网格实例打包好
- 若卡片捕捉 >= 128x128,将其划分为多个 128x128 的物理页
- 否则从 128x128 的物理页中分配一部分给它
-
第二趟:将卡片复制到表面缓存并压缩
接下来得考虑如何固定表面缓存中的光照。在计算命中点光照的时候,我们会遇到两个问题:
- 像素是否在阴影中
- 如何处理光线的多次弹射
Lumen 的解决流程如下(简化版):
-
根据表面缓存(和阴影贴图)计算每个像素的直接光照
- 将 128x128 的页划分为 8x8 的块
- 使用 8x8 的块来剔除光源
- 每个块选择前 8 个光源
- 1 位阴影掩码
-
假设一个块可以被多个光源照亮,那么可叠加光照效果
-
在世界坐标中计算上一帧的体素光照
- 得到的最终光照作为下一帧计算的依据
对于场景中较远的点,要知道光源命中该点时会进过哪些物体是比较困难的事,因为远处物体用到的是全局 SDF,它不记录网格信息,只记录命中位置和法线。因此使用体素光照来采样的结果如下:

解决方法是构建对于整个场景的辐射度缓存的体素裁剪图,具体来说是构建大小为 64x64x64 的 4 级裁剪图:
- 存储每个体素在 6 个方向上的辐射度
- 对法线采样并沿 3 个方向插值
- 裁剪图 0 覆盖 50 立方米的体积,且体素尺寸为 0.78 米
- 存储在 3D 纹理中
裁剪图更新频率的规则:
| 裁剪图0 | 裁剪图1 | 裁剪图2 | 裁剪图3 | |
|---|---|---|---|---|
| 起始帧 | 0 | 1 | 3 | 7 |
| 更新间隔 | 2 | 4 | 8 | 8 |
然后通过短射线投射(short ray cast)来构建纹素面:
- 在每个体素上沿 6 个方向追踪网格 DF
- 记录命中网格的 ID 和命中距离
- 射线起点 = 体素中心 - 轴向单位向量 × 体素半径
- 射线终点 = 体素中心 + 轴向单位向量 × 体素半径
接下来将空间划分为 4x4x4 的块(tile),以滤除大多数物体。
目前我们已经求出了空间中体素的表达和光线命中点,于是下一步便是求体素受到的光照,也就是将光注入到裁剪图中。
- 清除整个 Clipmap 中的所有体素光照
- 压缩 Clipmap 中所有有效的 VisBuffer
- 从 VisBuffer 中采样 FinalLighting 并注入光照
后面不想写了
因为我的理解能力有限 + 个人感觉后面实在讲的太乱了,所以实在没动力整理下去了 QAQ。也许后面会补上?
Phase 3: Build a lot of Probes with Different Kinds
Screen Space Probe
Importance Sampling
Denoising and Spatial Probe Filtering
World Space Probes and Ray Connecting
Phase 4: Shading Full Pixels with Screen Space Probes
评论区