【RTR4笔记】 第十一章 全局光照(上)

基础全局光照,Ambient Occlusion, Direction Occlusion

Posted Kongouuu's Blog on March 24, 2022

11.1 Rendering Equation

\[L_o(p,v)=\int_{l \in \Omega}f(l,v)L_i(p,l)(n \cdot l)dl\]

在前面的部分中我们经常用到上面的渲染方程去处理我们的光照。而这个实际的意义就是去计算 物体表面接受到光后,会传输多少光给眼睛 这个值。不过前面直接光照或者环境光照的模型本质上都没有考虑到场景中的其他的物体对光源接收的影响。顺便一提,前面提到的像阴影这样的东西都可以当作全局光照的一部分。

全局光照的主要研究方向就是计算光在直接光照以外的影响。一般来说物理上可能比较不正确,因为模型过于复杂。最常见的两个做法就是 简化与预计算 , 部分思路会在后面的部分中提及。

11.2 General Global Illumination

在全局光照的模型中,我们的渲染方程的光源输入 Li 将不单单是来自我们直接设定的光源,而也会取自其他物体发射的光。这种复杂的计算能够渲染出非常真实的画面,不过开销过大并不能用来做实时渲染。

一般来说解决渲染方程有两个大体思路,一个是有限元方法一个是蒙特卡洛方法。有限元方法下面会提到 辐射着色 方法, 而蒙特卡洛的例子则是比较出名的 光线追踪

11.2.1 Radiosity (辐射着色)

辐射着色思路的核心就是把所有的物体都分成若干小片光源,每个小片会接收其他小片发射的辐射度,并且自己也提供一些辐射。并且这里假设了我们的反射的光 (间接光源) 全都是漫反射的,这在特殊的场景下会非常的失真,不过常规的渲染场景下依旧是可行的。

\[每个物体的输出辐射度B_i为:\\ B_i=B_i^e + \rho_{diffuse}\sum_jF_{ij}B_j\\ B_i^e:物体本身反射出来的辐射\\ F_{ij}:从片段j到片段i的强度调整系数\\ B_j:片段j的辐射度\]

因为实际上里面每个片段都是会对互相影响的,不分先后,所以我们需要把这个场景设计成一个线性系统去做解算,之后就可以得到所有片段发射出的光。不过这个算法的问题也很大,他实在是太复杂了,场景东西多的话我们会分成大量的小片段,计算起来也并不是很迅速。

对于一整个场景来说这样开销确实是很大,就算提前计算好这些信息,在实时渲染中如果场景内有动态的物体最后也会没辙。不过这个思路后续还是会用到一些次要的设计的,后面会提到。

11.2.2 Ray Tracing

最常规的光线追踪办法就是我们从摄像机往投影平面的像素上发射光线,并记录碰撞点的颜色值。一般来说如果我们需要继续考虑反射或者折射,我们可以让光线在碰撞后继续前进。不过一般来说我们如果根据碰撞点的法线等信息去判断反射或折射的路径,我们只能得出非常锐利的结果,例如镜面反射和硬阴影;并且渲染方程里面的 积分 的概念也并不会纳入考量。

不过一般来说我们遇到物体后它并不会只往一个方向发射出射光,而是根据法线分布往对应的 lobe 前进,不过如果要计算一定范围内的所有出射光那又需要太多计算了。因此被研发出来的思路是蒙特卡洛算法,就是可以用少量的采样去模拟一个范围较大的积分的方式。通过使用蒙特卡洛我们可以确保光线在碰撞后可以往多个方向采样来达到更真实的结果。

为了达到真实的效果,我们需要更多的采样,不过太多采样会有过多的开销, 因为每一次弹射都会增加 N 个采样。 使用重要性采样可以解决一些问题,但不完全,实际上一条光线在碰撞后只要需求大于等于两个采样,时间复杂度都会飙升。

11.3 Ambient Occlusion

前面一部分提到的方法都是全面的去处理画面的全局光照情况。不过因为处理的过于全面,导致大部分情况下没办法轻易的拿来做实时渲染,因为开销过大。不过当我们把全局光照造成的结果一个一个拆开来看,我们可以更简单的在实时渲染的时候去达到特定的效果,例如环境光遮蔽 (Ambient Occlusion)。

11.3.1 Ambient Occlusion Theory

环境光遮蔽这里像名字一样,考虑的是环境光,也就是半球范围内都会有入射的这么一个情况。我们先考虑一个简单的兰伯特模型,也就是辐照度跟出射的光照值是成比的,并且假设入射的辐射率是一个常数:

\[E(p,n)=\int_{l\in \Omega}L_{ambient}(n\cdot l)^+dl=\pi L_{ambient}\]

我们可以很简单的计算出像素收到的辐照度是环境光强乘以PI 。不过一般来说我们场景的几何是相对复杂的,很多像素是没办法接收来自整个半球的环境光的,因为即使是环境光也会被其它几何遮挡住,因此我们的辐照度应该会变成:

\[E(p,n)=L_{ambient} \int_{l\in \Omega}v(p,l)(n\cdot l)^+dl\\ v(p,l): 点在这个入射光方向是否可见\]

因此我们可以演变出一个用于衡量有多少环境光被遮挡的系数kA:

\[k_A(p)=\frac{1}{\pi}\int_{l\in \Omega}v(p,l)(n\cdot l)^+dl\\ E(p,n)=k_A(p)*\pi*L_{ambient}\]

除了这个系数之外,还会考虑到一个叫 bent normal 的向量,用来代表没被遮挡的方向的平均值。比较基础的环境光遮蔽计算并不会去考虑它,不过它本身的性质在用于计算更真实的效果上还是很有用的。

11.3.2 Interreflections

一般来说我们的环境光遮蔽(左图)计算会产生出比使用完整的全局光照计算出来的情况(右图)要暗很多的画面。这主要是因为我们只单纯考虑了某个方向上的环境光是否被挡住,而没有考虑到遮挡物往像素反射的光照值。因为这里的场景还是在计算一个光线没有 Bounce 的情况,所以这样的结果是可以理解的。

这里Stewart 和 Langer 推出了一个非常快速的办法去计算这个损失的补偿:

\[E=\frac{\pi k_A}{1-\rho(1-k_A)}L_i\\ k_A\ '=\frac{k_A}{1-\rho (1-k_A)}\]

这里书上是简单题了一下这是考虑到我们的入射光也会包含其他表面的 Diffuse 反射的结果,并没有细推。不过实际上差不多也就这么一回事吧。

简单推一下:

\[k_A:可见度\\ 1-k_A:被遮挡度\\ \rho (1-k_A): 被遮挡的方向上,其他面片能反射进来的比例\\ 1-\rho (1-k_A): 没有来自其他面的反射的方向比例\\ \frac{k_A}{1-\rho (1-k_A)}: 把其他面的反射从半球积分内剔除后,可见度为多少\]

这里做了一个大胆的预测就是遮挡了像素的面的 Albedo 跟被遮挡的像素是相等的。不过由于其他面接受到的光也是很复杂的,考虑到其他面也会有遮蔽的问题,我们没有办法直接地去计算其他面到底应该反射多少入射光过来,这个系统会解算起来很麻烦。

虽然书里没有明确的说,但个人认为这个函数的做法就是: 遮挡处的反射太麻烦了,所以我们不管它 。 我们把可能有反射过来的部分从半球积分中剔除,重新调整一下我们的可见比例,就是这个做法的核心了(猜测)。

11.3.3 Precomputed Ambient Occlusion

环境遮蔽的一种办法是使用烘焙,就是提前计算好我们的遮蔽值。最简单的情况就是提前为场景使用蒙特卡洛积分进行光线追踪来为每个物体去计算它们的环境光遮蔽,不过这样的做法他是没有办法处理一个动态的场景。当然,这不代表预计算的东西是没办法处理动态场景的。

其中一个用来处理动态场景的预计算环境光遮蔽就是 Ambient Occlusion Fields (环境光遮蔽场) , 他就是提前为物体计算出一个 Cube Map, 可以查询当附近有物体时该表现出什么样的环境光遮蔽效果。当我们为静态的建筑生成类似于这样的场,在动态的物体靠近的时候就可以用动态物体相对于静态建筑的位置为建筑生成对应的环境光遮蔽的效果。

育碧的刺客信条使用的是一个叫 World AO 的方式;离线的从正上方为整个地图生成一个深度图,在运行的时候根据查询地方的高度就可以判断出遮挡物的高度,并且直接为这个俯视角的图生成 AO 系数。 之后我们会为场景所有的物体通过生成的 AO 系数套用这个遮蔽值。

11.3.4 Dynamic Computation of Ambient Occlusion

比起对静态物体操作,很多时候我们会希望动态的场景的动态物体也能享受到更多的环境光遮蔽效果。不过动态的场景下我们如果需要在每一帧都用光追来计算环境光遮蔽,那开销实在太大了,即使是用 GPU 计算也不合适。

Bunnell 的方法是使用大量的圆盘形状去模拟物体的表面,因为对圆盘之间的遮挡运算比对面片之间进行光追来的快很多。不过它也并不是很快,就算我们能通过特殊的手法把复杂度降低到 O(nlogn), 无论是效果还是效率都并不会很理想。

后来被开发的方法也有使用 SDF 来判断遮挡信息,或者使用体素。或者说把物体看成大量的球体,并且用球谐函数来表达可见度信息。这么多不同的做法本质上都是在解决一个问题是,就是避免为顶点进行多方向的光追采样;不如说,从根本上来看,环境光遮蔽就是检查物体遮挡,他们做的都是在简化场景信息来做到更简单的遮挡查询。

11.3.5 Screen-Space Methods

上面的办法的复杂度都是基于场景复杂度来决定的,也就是说是不可估算的。在比较复杂的场景下可能会过度开销,导致运行不顺畅。不过,在后面的研究中,环境光遮蔽也可以从屏幕信息中获得,而用屏幕信息来计算就不需要考虑到场景的复杂度。

先聊一下 Crysis(孤岛危机) 所使用的,最早的屏幕空间的环境光遮蔽计算方式 SSAO。 他只使用了一个几何信息,就是深度。实际做法就是在渲染一个点的时候,随机的选取以那个点为中心的圆球范围内的点,并且跟深度图去对比;如果采样的点比深度图深,那么就增加遮蔽程度。使用上来说,非常的简单,因为我们不需要几何信息,只需要深度,而且跑的也可以很快,乍看之下效果还可以。不过问题就在于我们没有法线,没法考虑半球信息,考虑圆球信息时,对平面下方的点的采样也会让平面变深,这是很不合理的…

后续也有很多方法单单根据深度图去进行,不过不久就有人开始使用法线信息了。使用法线的算法就可以去考虑表面法线方向的半球范围内的信息,而不用根据整个环绕的球体来,书中提到的第一个是 Volumetric Ambient Occlusion 。 这个做法首先是判断出要采样的半球,但由于积分算起来是比较麻烦的,为了避免这个计算,他在法线方向上重新画了一个球体。并且直接的计算球体内多少采样被遮挡,多少没有,如下图:

类似的做法还有一个 Horizon-Based Ambient Occlusion (HBAO) ,也叫水平基准环境光遮蔽, 知乎有个很好的 参考文章 。 主要思路就是为一个点选取几个角度做 Ray Marching, 根据每个方向的遮挡物判断出每个方向的遮挡基于原点的角度。 之后再计算我们步进方向投影到表面上的切线,然后根据两个角度的差去计算有多少遮挡。这里要用多个方向去采样,越多越准确。

HBAO 的缺点就是不考虑不同方向的 Cosine Term, 也就是法线点乘入射方向这个值。我们常规的基于物理渲染中一定要考虑这个的,因为光线如果不垂直于平面是没办法提供全额的能量的。 前面的做法大部分都没处理这个特性来计算遮蔽,虽然看起来不错不过并不会很物理正确。为了改进这一点, Ground-Truth Ambient Occlusion (GTAO)HBAO 上额外加入了这个项的考量。

11.3.6 Shading with Ambient Occlusion

上面的所有计算都是为了得出一个遮蔽系数 kA。 我们拿简单的模型说一下:

\[L_o(v)=\int_{\Omega} f(l,v)L_i(l)v(l)(n\cdot l) dl=\\ [兰伯特BRDF]\int_{\Omega} \frac{\rho}{\pi}L_i(l)v(l)(n\cdot l)dl\ =\\ \frac{\rho}{\pi} \int_{\Omega} L_i(l)v(l)(n\cdot l)dl =\\ \frac{\rho}{\pi} \frac{\int_{\Omega} L_i(l)v(l)(n\cdot l)dl}{\int_{\Omega} v(l)(n\cdot l) dl}\int_{\Omega} v(l)(n\cdot l)dl =\\ k_A*\rho * \frac{\int_{\Omega} L_i(l)v(l)(n\cdot l)dl}{\int_{\Omega} v(l)(n\cdot l) dl}\\ since\ k_A=\frac{\int_{\Omega} v(l)(n\cdot l)dl}{\pi}\\ L_o=k_A*\rho * \frac{\int_{\Omega} L_i(l)v(l)(n\cdot l)dl}{\int_{\Omega} v(l)(n\cdot l) dl}=k_A*\rho *E\\ E:提前准备好的Irradiance\ Map采样的值\]

实际上,就是我们采样好提前处理好的环境光后直接乘以我们的遮蔽系数就可以了。

11.4 Directional Occlusion

上面提到了很多种环境光遮蔽的计算方法,不过他们都没有办法应付一些比较特殊的情况。我们举一个例子,假设天空一半是红色的一半是绿色的,我们使用常规的办法去计算遮蔽,无论遮挡物在像素的什么方向,最后颜色一定都还是考虑红绿色的均值,而不会根据遮蔽方向去选择正确的环境光。这个问题是很好理解的,一方面我们的环境辐照度他确实是提前算好的均值,另一方面我们的遮蔽权值并不会跟方向一起考虑;而是分别算遮蔽值和辐照度,最后再加一起。

使用 Bent Normal 来决定采样的辐照度方向可以适当的解决这个问题,但是不完全。主要是因为即使 a 点的右侧被完全遮挡,一般来说我们计算的 Bent Normal 依旧会把遮挡物右边的部分当作 可以对a有入射光的方向。 不过这个问题我们可以使用 Directional Occlusion 解决。

11.4.1 Precomputed Directional Occlusion

预计算遮挡信息的方式大多都是为一个几何自己的形状去提前计算哪些方向有遮蔽。基本上都是通过不同的表达方式去表明几何上的每个像素在哪个方向被遮蔽,并且应该往哪个方向去采样。预计算最多也只能是做到这样的操作了,对应动态的场景,是没有办法提前计算好动态物体与其他物体形成的遮蔽。

11.4.2 Dynamic Computation of Directional Occlusion

动态的遮挡信息计算其实跟上面屏幕空间的部分基本都是差不多思路的。 在屏幕空间的环境光遮蔽计算,我们都是通过屏幕信息去采样并且计算有多少遮蔽值。我们只需要在计算完遮蔽值的同时,根据已有的采样去判断应该往什么方向去进行辐照度的采样。有很多不同的表达方式,比如说 SH, 或者锥形,不过基本上思路都是大同小异。

这些思路上并不会差差太多,唯一的缺点就是计算量的提升。如果我们只需要计算遮蔽的程度,可以不考虑方向,就可以使用像 VAO 那种单纯靠多次对比来达到的效果;一旦我们需要考虑到方向性等问题,可能就需要进行像 HBAO 那样的 Ray Marching, 并且在那之上进行更多的采样和计算。

要特别注意,这里目前提到的都只是遮挡信息,而非着色。也就是说这里我们只考虑如何储存 [哪个方向被遮蔽了] 而不考虑怎么去采样真实的光照

11.4.3 Shading with Directional Occlusion

普通着色

在步入环境光之前,先聊一下方向遮蔽对普通光,甚至面积光。很显然我们是可以很直接的通过遮蔽信息去判断是否应该接收一个普通光的光照。对于面积光来说,只需要把积分限定在面积光的范围内就可以简单的通过采样来达到我们的效果了,这里其实也没有什么好说的。跟阴影贴图是很相似的,因为有几何信息,所以我们可以用几何信息在采样或计算的过程中去除外一些不该考虑到的光源。

环境光着色

环境光相较来说是非常复杂的,因为它会从半球方向进来。直接的对整个半球去进行采样,并且考虑遮蔽信息多半会产生出不够好的结果,因为采样数量多的话会影响效率,少的话也没有什么意义。环境光这里我们先考虑一下漫反射的情况:

\[L_o=\frac{\rho}{\pi}\int_{l\in \Omega}L_i(l)v(l)(n\cdot l)dl\\ v(l):方向遮蔽提供的可见度信息\]

往常的环境光做法中我们会提前计算好 (Li(l)(n·l)) 这个信息,储存在我们的辐照度图中,然后这个值就可以当作常数,这样我们一次采样就能得到环境光的值。不过在现在多了遮蔽的可视度变量后,往常的办法是不行的。如果我们能把需要计算的项控制在两个,也就是说两个函数;并且使用类似 球谐函数 的储存方式,那么可以很直接的通过点积来得到两个积分的值。

简单来说就是通过下面两个办法:

  1. 合并余弦项(n·l) 和遮蔽项 (v): 我们在计算出遮蔽项的时候同时可以把法线方向纳入考虑,并且储存起来。如果这个信息是用球谐函数的方式储存,光照也是球谐函数,那么两者点乘就可以得到我们需要的积分的值。缺点是法线在后续计算中不能有变化
  2. 合并余弦项(n·l) 和光照项 (L): 这个我们要么是提前用大量的储存去计算不同法线的结果,要么是运行时计算,不过那样开销太大。

不过如果我们的遮蔽信息,或者说可视信息是使用的像锥形的时候。我们可以提前根据不同的锥形大小去形成不同程度的环境光滤波,并且直接的通过单次采样得到结果。不过这个做法的问题很明显, 我们可视方向的锥形的中心向量,大概率不会是我们像素点的法线;但我们采样的时候却是把这个当法线来去采样。物理上这样做肯定是错误的,不过视觉上还过得去。

光滑的表面比漫反射的复杂的要多,不过因为计算开销问题也并不会很常使用。

Extra1 Screen Space Directional Occlusion

从本质上来看, 无论是 AO 还是 DO, 都只是对于 几何上的遮蔽信息 的计算,而跟任何的着色是没有绑定关系的。我们可以使用这些办法得到的遮蔽信息去进行我们想要的着色效果,例如我们使用从屏幕空间得到的一些遮蔽信息去计算环境光的遮蔽,然后把这个操作称作 SSAODO 这里也是,本质上在 DO 范围内的就是通过不同的办法去得到一个含有方向的遮蔽信息,他跟着色是没有绑定的。上面有提到, DO 给的信息可以帮助我们去进行一些直接光,或者环境光的计算,不过同时也可以达到更多的效果。

SSDO 是现在比较流行的一个 DO 用来实现特定效果的操作,详细可以参考文章SSDO 本质上跟 HBAO 是十分相似的,不过除了计算遮蔽信息来计算类似阴影的效果以外, 还额外计算了Light Bleeding效果(模拟光线弹射)。因为是屏幕空间,所以他并不是像前面章节提到的那种通过遮蔽方向选取正确的 环境光 的算法,而只是单纯的计算了 遮蔽+间接光 的一个算法。