跳转至

3 Real-Time Global Illumination

3.1 Reflective Shadow Mapping

要做全局光照,那么我们不仅需要考虑直接光照,也要考虑间接光照。

Reflective Shadow Mapping (RSM) 是一种用于实时渲染中模拟一次间接光照(全局光照)的技术。我们利用阴影贴图,记录那些第一次反光点,然后再去照亮所要渲染的点,从而模拟一次间接光照。

那么此时点 p 被点 q 照亮并反射到相机的光为

\[ L_o(\mathbf{p}, \omega_o) = \int_{\Omega_{\text{patch}}} L_i(\mathbf{p}, \mathbf{q} \to \mathbf{p}) \, V(\mathbf{p}, \mathbf{q} \to \mathbf{p}) \, f_r(\mathbf{p}, \mathbf{q} \to \mathbf{p}, \omega_o) \, \cos\theta_i \, d\omega_i \]

然后由上图的公式,将其积分域换到光源的面积上,得到

\[ L_o(\mathbf{p}, \omega_o) = \int_{A_{\text{patch}}} L_i(\mathbf{q} \to \mathbf{p}) \, V(\mathbf{p}, \mathbf{q} \to \mathbf{p}) \, f_r(\mathbf{p}, \mathbf{q} \to \mathbf{p}, \omega_o) \, \frac{\cos\theta_p \, \cos\theta_q}{\|\mathbf{q} - \mathbf{p}\|^2} \, dA \]

然后我们计算的时候做以下假设

  • 计算第一次反射的情况,材质都为 diffuse 的,那么 \(f_r = \frac{\rho}{\pi}\)
  • 我们认为所有的点到 \(\mathbf{p}\) 都是可见的,也就是说 $V(\mathbf{p}, \mathbf{q} \to \mathbf{p}) $ 均为 1

辐射度量学中的相关概念,我们可以得到

$$ L_i = f_r \cdot \frac{\Phi}{dA} $$ 其中 \(\Phi\) 是照到 \(\mathbf{q}\) 上的光照通量 incident flux


对于上述可见性的假设,其实可以做一定的优化。我们有一个观察,就是一般在这个要渲染的点附近的间接光源不会被挡住,也就是说找在世界坐标下离 \(\mathbf{p}\) 较近的点,但是直接去找世界坐标近的点,需要的计算量比较大,所以我们直接假设在 shadow map 中离 \(\mathbf{p}\) 相应位置较近的点就是在世界坐标下离 \(\mathbf{p}\) 较近的点

还有就是如果 shadow map 比较大,那么每计算一个点,都要计算 shadow map 中全部的像素对该点的影响。那么我们可以对这些像素进行采样,选择一些点来照亮该点。


总结一下,RSM 需要在 shadow map 中存储

  • 深度
  • 世界坐标
  • 法线方向
  • 光通量 flux

3.2 Light Propagation Volumes

我们要渲染一个点,需要希望可以从任意方向查询辐射度(radiance),这样我们就可以直接去渲染该点。

我们的思路是将场景视为一个一个的体素,然后每个体素都去存储一组 “方向性辐射度”,这样在渲染时,根据像素所在的位置,从对应的网格单元中插值就可以获取该点的间接光照。


LPV 流程的起始阶段,目标是识别并采样场景中所有被光源直接照亮的表面区域

  • 使用 RSM 得到哪些表面被照亮,还有相应的颜色和朝向
  • 为了效率,我们不会对每一个像素都进行处理,而是对 RSM 中的结果进行下采样或聚类,提取出代表性的“表面块”(patches)
  • 每个这样的 patch 被视为一个虚拟点光源(virtual light source),它会向周围空间发射光线(在后续步骤中注入到 3D 网格)


上一步我们生成了许多“虚拟光源点”(即直接受光的表面 patch)。现在要判断哪些虚拟光源落在当前网格单元内部(或其影响范围内)

每个虚拟光源不仅有一个强度值,还有一个方向分布(即它在各个方向上发射多少光)。在一个网格单元内,如果有多个虚拟光源,我们需要把它们的辐射度按方向叠加起来。

为了高效存储和运算,LPV 不使用原始的方向采样(如 6 面或更多方向),而是用球谐函数对方向性辐射度进行压缩表示。

黄色灯泡照到蓝色物体上,粉色的部分是虚拟光源,然后再将每个格子的虚拟光源合并起来,然后再用球谐函数压缩表示


这一步通过迭代方式,在每个网格单元之间传递辐射度信息,从而近似模拟光线在场景中多次反射、散射后形成的全局光照效果。

对每个网格单元,收集它从六个面(前/后/左/右/上/下)接收到的辐射度

  • 每个网格单元有六个相邻的邻居(在 3D 空间中),分别对应 ±X, ±Y, ±Z 方向。
  • 在每一步传播中,当前单元会从这六个方向“接收”来自邻居单元的辐射度。
  • 注意:这里的“接收”不是物理意义上的入射角或遮挡判断,而是简单地将邻居单元的辐射度“流入”当前单元

假设光可以无阻碍地在网格间流动(除非被几何体阻挡,但基础 LPV 不处理遮挡)。

然后将接收到的辐射度求和,并再次用球谐函数(SH)表示。重复此传播过程若干次,直到体积内的辐射度分布趋于稳定。

  • 初始时,只有直接受光的单元有能量;经过几次传播后,能量逐步扩散到远处单元。
  • 当连续两次迭代之间的变化小于某个阈值(或达到预设最大次数),就认为系统“稳定”,停止传播。
  • 实际应用中通常迭代 5~10 次即可获得视觉上可接受的效果


在渲染时,对于屏幕上的每一个像素,我们需要知道它在 3D 空间中的位置。然后根据这个位置,映射到预定义的 3D 网格中对应的体素单元。

每个网格单元在上一步传播完成后,已经存储了一个方向性辐射度分布,然后利用提取出的方向性辐射度,结合当前着色点的法线、材质属性(如漫反射系数),通过积分或近似公式计算出最终的间接光照贡献。

但是这个方法存在问题,这在传播的时候并没有考虑遮蔽的问题,比如说下面这个点 p,其位于障碍物内部或紧贴其表面。红色箭头表示从点 p 向左右两侧发出的光线方向。但是点 p 实际上被障碍物挡住了,不应该接收到来自另一侧的光!

这就会导致 "漏光问题",这是 LPV 最严重的视觉缺陷之一。

3.3 Voxel Global Illumination (VXGI)

这个方法目的是在动态场景中实现高质量、实时的间接光照效果,支持漫反射 + 镜面反射(部分版本),并解决 LPV 的“漏光”等问题。

VXGI 继承了传统 GI 方法(如 RSM、LPV)的基本结构:

  • 第一遍(Pass 1):从光源视角或相机视角生成场景的几何与光照信息(如深度、法线、辐射度)。
  • 第二遍(Pass 2):利用这些信息进行间接光照计算(如光线追踪、 cone tracing 等)。

VXGI 的第一遍(Pass 1)从光源视角出发,首先渲染场景并将入射辐射度与光照方向信息“烘焙”进层次化体素结构(如八叉树)中,同时记录每个体素内的法线分布

随后在体素层级上进行滤波处理,对辐照度值和光方向进行平滑与聚合,以构建稳定、多分辨率的全局光照场。

多级滤波:为了让数据更好用,系统会对这些原始数据进行“平滑处理”。它会把小格子的信息向上合并成大格子的平均值

VXGI 的第二遍(Pass 2)——从相机视角出发,对每个着色点进行锥体追踪(Cone Tracing)以获取间接光照。

对于有光泽的表面,系统不再发射单条光线,而是沿反射方向发射一个锥形光束(Cone),随着锥体向前传播,它的覆盖范围(Footprint)会越来越大。系统会根据锥体当前的粗细,自动在第一步建立的“多级数据库”中查找最匹配的层级。

  • 锥体细(近处/细节)→ 查高分辨率的小体素;
  • 锥体粗(远处/模糊)→ 查低分辨率的大体素。

最后,系统通过数学插值(四线性插值)将沿途采样的多个数据点柔和地融合在一起。这样既算出了清晰的镜面高光,又得到了柔和的环境反光,且速度极快,无需逐像素暴力计算。

对于 diffuse 的情况,可以计算多个锥体,然后综合得到结果。(下图的蓝色部分是 diffuse 的情况,红色部分是 specular 的情况

与 RSM 的区别:

  • RSM 直接依赖光源视角的 2D 像素采样,容易受分辨率限制和视角遮挡影响导致精度不足;而 VXGI 将直接受光表面转化为层次化体素(如八叉树),在 3D 空间中存储几何与辐射度信息。
  • RSM 采用在 2D 纹理上进行半球随机采样的方式,本质是投影查找,极易产生噪点和误差;VXGI 则改为在 3D 体素空间中执行反射锥体追踪(Cone Tracing),即从着色点发射锥形光束穿过体素网格并累加沿途辐射度。

3.4 Screen Space Ambient Occlusion (SSAO)

之前我们考虑的都是 3D 空间下的结果,这里介绍的 SSAO 则是基于屏幕空间下的环境光遮蔽。

对于任意一点,我们想要得到间接光照,假设对于任意一点任意方向的间接光照都是定值,但是我们要考虑 visibility 项。一些容易被遮蔽的地方,打到里面的光就比较少,就会显得比较暗。

回到之前的渲染方程

\[ L_o(\mathbf{p}, \omega_o) = \int_{\Omega^+} L_i(\mathbf{p}, \omega_i) \cdot f_r(\mathbf{p}, \omega_i, \omega_o) \cos \theta_i\cdot V(\mathbf{p}, \omega_i) \, d\omega_i \]

可以将上述渲染方程拆分为

$$ L_o^{\text{indir}}(\mathbf{p}, \omega_o) \approx

\frac{ \int_{\Omega^+} V(\mathbf{p}, \omega_i) \cos\theta_i \, d\omega_i }{ \int_{\Omega^+} \cos\theta_i \, d\omega_i }

\cdot

\int_{\Omega^+} L_i^{\text{indir}}(\mathbf{p}, \omega_i) f_r(\mathbf{p}, \omega_i, \omega_o) \cos\theta_i \, d\omega_i $$

其中,前面这一项记为 \(k_A\),值为

\[ k_A =\frac{ \int_{\Omega^+} V(\mathbf{p}, \omega_i) \cos\theta_i \, d\omega_i }{ \pi } \]

这个代表的是按每个方向对表面的实际光照贡献大小进行加权平均。

因为对于漫反射表面(Lambertian),光线越垂直于表面,贡献越大;越倾斜,贡献越小。这是由 BRDF 和几何投影决定的物理事实。

后面这一项则是

\[ L_i^{\text{indir}}(p)\cdot \frac{\rho}{\pi}\cdot \pi = L_i^{\text{indir}}(p)\cdot \rho \]

这是一个常值,代表的是 AO。

然后我们说明一下为什么上面这个式子是对的,实际上,上述式子是从投影立体角的视角去看待的

进行换元即可得到相应的式子,至于为什么拆分是正确的,因为后面一项 AO 是常数,这实际上可以认为是一个等式。


实际上,这里可以将渲染方程看得更加简单,上面的推导拆分只是为了能够更深地理解,因为 brdf 和 L 是常数, 所以可以直接提取出来。

\[ \begin{align*} L_o(\mathbf{p}, \omega_o) &= \int_{\Omega^+} L_i(\mathbf{p}, \omega_i) \, f_r(\mathbf{p}, \omega_i, \omega_o) \, V(\mathbf{p}, \omega_i) \, \cos\theta_i \, d\omega_i \\ &= \frac{\rho}{\pi} \cdot L_i(\mathbf{p}) \cdot \int_{\Omega^+} V(\mathbf{p}, \omega_i) \, \cos\theta_i \, d\omega_i \end{align*} \]

我们实际上需要计算的只是 \(k_A(p)\)

  • 物体空间法通过向场景几何体发射光线(Raycasting)来精确判断可见性,但速度慢、依赖复杂的空间数据结构或简化模型,且性能随场景复杂度上升而下降;
  • 屏幕空间法在渲染完成后作为后处理步骤执行,无需预处理、不依赖场景几何复杂度、实现简单高效,但其结果基于深度缓冲近似,缺乏物理准确性,易受视角和分辨率影响

SSAO 采用的是屏幕空间下的 \(k_A(p)\) 的估计,利用深度图,采样着色点周围的一个球体内,有多少是比记录的深度是要深的,从而近似地估计该着色点的遮蔽情况

其中红色的点的深度是比深度图记录的深度要深的,由上图可以明显感受到,绿色的点越多,该着色点越不会被遮蔽。但由于如果是一个平面的话,初始就有一半的点是红色的,所以这个估计只在红色的点多于一半的时候开始考虑环境光遮蔽。

该技术由于仅依赖深度图(z-buffer)来近似场景几何,缺乏真实的三维结构信息,因此会产生错误的遮挡判断(False Occlusions),例如图中红线所示,位于当前表面“后方”但在投影空间重叠的采样点会被误判为遮挡物。此外,为了追求实时性能,SSAO 通常忽略物理上必要的余弦权重( cos⁡θcos*θ* ),即不对不同入射角度的采样点进行加权,导致计算结果在物理上并不准确。

可以看到上面这个图中,橙色圈中就出现了不正确的遮挡,

在采样的过程中,采样梳理需要在精度与性能间权衡:虽然更多采样点能提升结果准确性,但为保证实时性通常仅使用约 16 个样本。但这会引入噪点,因此需通过保边模糊(如双边滤波)进行后处理,在平滑噪声的同时保留清晰的物体边缘,从而获得干净自然的最终画面。


HBAO(Horizon-Based Ambient Occlusion)是一种在屏幕空间内实现的环境光遮蔽技术,它通过近似地对深度缓冲区进行“射线追踪”来计算遮挡:与基础 SSAO 不同,HBAO 利用已知的表面法线信息,仅在以该法线为轴的半球空间内采样,并沿多个方向寻找水平角,从而更精确地估算可见性;

这种方法比随机采样更符合物理原理,能产生更清晰、更少噪点的 AO 效果,但计算成本略高,且依赖于法线缓冲区的可用性。

左图是使用 SSAO 得到的结果,右边是 HBAO 得到的结果,很明显看到 HBAO 在 SSAO 未正确处理的遮挡关系中处理的很好

3.5 Screen Space Directional Occlusion (SSDO)

在 SSAO 中我们假设入射的间接光是均匀的,我们实际上可以利用屏幕上部分的间接光的信息,引入遮挡计算,使得 AO 效果能够一定程度反映真实光照。

整个基本原理与路径追踪很像,在每个着色点 pp 发射一条随机射线,若该射线未击中任何障碍物,则说明该方向可直接接收光源 → 贡献直接光照;若击中了其他表面,则从命中点获取其颜色或亮度作为间接光照来源。

比较 SSAO 和 SSDO

SSAO 中红色部分来计算间接光照,SSDO 则是橙色部分来计算间接光照

\[ \begin{align*} L_o^{\text{dir}}(\mathbf{p}, \omega_o) &= \int_{\Omega^+, V = 1} L_i^{\text{dir}}(\mathbf{p}, \omega_i) \, f_r(\mathbf{p}, \omega_i, \omega_o) \, \cos\theta_i \, d\omega_i \\ L_o^{\text{indir}}(\mathbf{p}, \omega_o) &= \int_{\Omega^+, V = 0} L_i^{\text{indir}}(\mathbf{p}, \omega_i) \, f_r(\mathbf{p}, \omega_i, \omega_o) \, \cos\theta_i \, d\omega_i \end{align*} \]

在每个着色点 P 的局部半球空间内对多个采样点(如 A、B、C、D)进行深度测试,判断其是否被遮挡;但是这个方法只是一个近似的方式,就像下图中第三幅图中,A 点对 P 点应该无间接光照,但是用该方法计算的时候,却认为 A 点能为 P 点提供间接光照; B 点则相反,都出现了错误。

虽然其渲染质量已接近离线渲染效果,能呈现更真实的间接光照和色彩渗透,但仍存在几个问题

  • 首先,它仅能模拟短范围的全局光照(GI),无法处理远距离光线反弹;

比如说上图种正方体右侧面的绿色是右边墙映过来的,这么远的间接光在 SSDO 中无法得到

  • 其次,可见性判断仍受限于屏幕空间数据;最关键的是,作为屏幕空间技术,它完全依赖当前帧可见的像素信息,对于摄像机视角下“不可见”的表面(如物体背面或被遮挡区域),无法获取其几何或光照数据,导致这些区域的光照计算缺失或错误

3.6 Screen Space Reflection (SSR)

屏幕空间反射(SSR)是一种在实时渲染中引入全局光照效果的技术,它通过在屏幕空间内执行“光线追踪”来模拟镜面反射——无需依赖原始 3D 几何体(如三角形),而是直接利用已渲染的深度、法线和颜色缓冲区。

基本过程如下:

  • 求交,即从着色点沿反射方向发射射线,在屏幕空间中与场景深度图进行步进式相交检测,找到命中像素;
  • 着色,将命中像素的颜色/亮度作为反射贡献返回给原着色点

最终的实现目标就是生成高质量的“镜面反射”

这些反射的内容很多都在屏幕中都展现出来了

第一步求交的方式使用的是 Ray Marching 的方式,可以选择一个像素一个像素地走。但是这肯定不是我们想要的,因为这样肯定非常慢。可是我们又没有像 SDF 那样的工具来告诉我们可以安全地走多少步。

这里我们可以利用 Depth map 中的信息来试探性地得到安全步数。利用 Depth map,生成相应的 mip-map,但是这里生成的 mipmap 并不是取平均值,而是取这片深度的最小值。这样我们在 marching 的时候,可以试探性地走大步,如果走大步的时候没有碰到物体,那么和原来的图也不会有交。

mip = 0;
while (level > -1)
    step through current cell;
    if (above Z plane) ++level;
    if (below Z plane) --level;

这里的试探与 TCP 中的拥塞控制很像

Hierarchical tracing

这是我们要 marching 的光线

第一步先走一步,发现没有碰到,那么下一步选择走的步数更大

这一步也没有碰到,下一步选择更高层的 mipmap

这一步碰到了,由于是在后半部碰到,所以前半部分也可以安全 marching,然后到第一层的 mipmap 中去查

接着往后查,最终得到交点


这个方法的问题与所有的屏幕空间方法是一致的,就是忽略了屏幕空间中没有的信息

比如说上面这张图中的反射图像,手的后半部分由于屏幕空间并没有看到,所以手的后半部分没有显示在倒影上

这张图则是窗帘上半部分没有拍到,但是应该在倒影中有所显示,就会出现上面的问题。可以选择模糊远一些的edge,这样看上去更加自然


这个方法可以实现

  • Sharp and blurry reflections(清晰与模糊反射):这个根据不同的 brdf 采样即可实现
  • Contact hardening(接触处硬化):这个就是 PCSS 中实现的效果。使用这个方法也自然而然的实现了,因为反射一个锥体的时候,越远锥体采样的面积就越大,也就越糊;越近的话,采样的面积就笑,那么就比较清晰。
  • Specular elongation(高光拉伸 / 反射畸变)
  • Per-pixel roughness and normal(逐像素粗糙度与法线)

在实现的时候,还有一些小技巧,比如说利用周围邻居的 tracing 结果的信息

或者说和之前 IBL 中 split sum 一样,引入 prefilter 等操作,预处理一些项,运行的时候只需要查询就行,但是实现起来会比较复杂。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!