GAMES104课程笔记07-Render Pipeline, Post-process and Everything
这个系列是GAMES104-现代游戏引擎:从入门到实践(GAMES 104: Modern Game Engine-Theory and Practice)的同步课程笔记。本课程会介绍现代游戏引擎所涉及的系统架构、技术点以及引擎系统相关的知识。本节课主要介绍现代游戏引擎中的渲染管线以及其它常用的技术。
Ambient Occlusion
环境光遮蔽(ambient occlusion, AO)是现代游戏必备的渲染技术,通过AO可以获得更加丰富的光影变化以及更加立体的视觉感受。


从渲染方程的角度来看,AO的本质是计算网格上每个点在光照下的可见性。

早期的AO是通过对网格进行预计算来实现,我们可以把顶点的可见性事先烘焙到网格信息中以便实际渲染时调用。这种方法目前在游戏工业中仍然有很多的应用。

SSAO
显然这种AO方法只考虑的单个模型自身的几何信息,当把模型放置在场景中时是无法得到正确的AO效果的。为了克服这样的问题,人们提出了SSAO这种基于屏幕空间的AO算法。SSAO的处理方法是首先从相机出发渲染出一张深度图,然后对于深度图上的每一个像素我们在屏幕空间上找到该点对应的模型位置并在它的周围采样出\(N\)个点。利用这\(N\)个采样点以及深度图我们就可以估计该点处的遮挡关系,从而实现AO的效果。

当然SSAO是一种近似方法,它本身是存在一些问题的。在SSAO的基础上人们又发展出了SSAO+算法,SSAO+没有在整个球体内进行采样而是根据顶点法向考虑法向对应的上半球上进行采样。这种改进克服了SSAO容易产生的整个游戏画面过暗的问题。

使用SSAO+实现的AO效果可见下图。SSAO相关方法的缺陷在于它只考虑了屏幕空间上的几何,这种简化在某些情况下会导致错误的结果。


HBAO
HBAO同样是基于屏幕空间的AO算法,它的思想是使用天顶角\(\theta\)的可见性来代替SSAO中的采样方法。我们可以通过对顶点法向上半球的方向通过ray marching的方式进行积分来估计它的可见性。


GTAO
上面介绍的几种算法都隐含地假设了顶点接收到来自不同方向上的光照是一致的,但实际上这种假设都没有考虑到渲染方程中的余弦项\(\cos \theta\)。GTAO补充了缺失的余弦项,此外对光线进行多次弹射的效果进行了拟合。


Ray-Tracing Ambient Occlusion
当然,利用现代GPU的强大计算能力我们也可通过real time ray tracing的方式来计算AO。

Fog
Depth Fog
雾效是游戏设计者非常喜欢的视觉效果。最简单的雾效是使用深度信息来考虑雾的视觉效果。

Height Fog
当然基于深度的雾效只有有限的表达效果。现实中的雾往往和它所处的高度有关,海拔高度越低的地方雾的效果越明显。对于这种视觉效果可以使用高度信息来进行近似:当高度大于一定的阈值时雾的效果会呈指数递减。

Voxel-based Volumetric Fog
在现代3A游戏中雾效可以使用体素化的方法来进行表现。在进行渲染时需要利用ray marching的方式考虑光线在参与介质中的各种行为,这样就可以获得逼真的雾效。

Anti-aliasing
走样(aliasing)是渲染中非常容易出现的问题。当相机的采样频率小于场景变化的频率时就会导致图像上出现各种各样的锯齿和走样。

反走样(anti-aliasing)的目标是去除掉图像上的走样,它的基本思路是在每个像素点上进行多次采样并取平均,这样就可以过滤掉高频信号。

SSAA and MSAA
早期的反走样方法是直接使用更高的分辨率进行渲染,然后在输出图像前再通过降采样的方法来获得正常分辨率的图像,这样的方法称为超采样(super sampling)。显然超采样的方法会导致非常大的计算复杂度,在现代游戏引擎中基本已经弃用。类似地,MSAA则是在渲染时利用采样点进行着色然后通过取平均的方式来处理走样的问题。

FXAA
SSAA和MSAA的主要问题在于超采样的过程导致了巨大的计算复杂度,而FXAA则是直接在原始分辨率的图像上进行反走样。FXAA的思想是在图像的边缘区域使用插值的方式来实现反走样,因此FXAA首先需要使用边缘检测算子来检测出图像亮度发生剧烈变化的区域。

然后通过卷积运算,FXAA判断这些区域是在水平方向还是垂直方向发生了较大的变化并以此作为反走样补偿的方向。得到补偿方向后还需要根据相邻像素之间的亮度差值来选择亮度差异大的作为具体的方向。

接下来FXAA会沿补偿方向的垂直方向寻找像素对,如果相邻像素在补偿方向上的差异与当前像素接近则与当前像素为同一组。通过向两边进行查找的方法来获得插值计算的端点。

得到端点后可以根据当前点到两端的距离来确定插值的系数。

最后对同一组中的像素进行颜色插值即可。

FXAA的反走样效果如下:

TAA
除了上面介绍过的方法外另一种进行反走样的思路是利用时序信息进行反走样,这类方法称为TAA。TAA的思想是考虑每个像素在上一帧所处的位置,然后将上一帧对应位置的颜色和当前帧上的颜色进行加权平均来进行滤波。目前很多游戏引擎都使用了TAA相关的方法来实现反走样。


Post-process
在现代3A游戏中对画面进行渲染后一般还需要添加各种后处理来提升画面的表现力。

Bloom
bloom是一种非常常见的灯光效果,在光源的四周我们往往可以看到一圈放大的光晕。从物理的角度上讲,bloom的成因在于真实相机的镜头并不符合完美的针孔相机,因此在成像时会出现这样的光学现象。

想要实现bloom的效果也非常简单,我们可以通过对渲染后的图像进行滤波来获得类似的效果。首先,我们需要找出图像中高亮度的区域。

然后我们对这些高亮的区域使用高斯模糊来获得bloom的效果。实际处理时往往还会对高斯核进行分解并且结合图像金字塔来进行加速。


最后,我们把高亮的区域叠加到原始图像上就可以得到bloom的效果。


Tone Mapping
色调映射(tone mapping)是输出最终渲染图像前的一个重要环节。目前3A游戏的光照往往都是HDR的,这容易导致渲染出的图像上会出现过明或过暗的区域。通过色调映射我们可以把HDR图像的亮度进行压缩来获得更好的显示效果。

色调映射的实现同样非常简单,我们只需要利用一条曲线对亮度进行缩放即可。filmic曲线就是色调映射中常用的一种曲线,它可以让游戏画面获得电影般的质感。

目前3A游戏中更多地使用了ACES曲线来进行色调映射。ACES来自于电影工业大量专业视觉工作者的总结,使用ACES曲线不仅会让画面更有表现力,而且它对于不同的显示设备都有很好的支持。


使用不同的常用曲线进行色调映射的效果如下。

Color Grading
颜色分级(color grading)是游戏设计者非常常用的一种调色方法,它的本质是建立一个颜色到颜色的映射从而获得不同的画面表现效果。

我们可以使用3D texture或者texture array等技术将这个映射存储在纹理中,然后通过查表的方式对画面进行调色。

因此对于游戏引擎来说则需要开发相关的模块来方便设计师建立这样的颜色映射。

通过color grading可以烘托玩家不同的情绪,从而提升玩家的游戏体验。

Rendering Pipeline
Forward Rendering
到目前为止我们已经学到了很多工业界常用的渲染算法,在实际进行渲染时还需要通过渲染管线(rendering pipeline)来将这些算法组织起来。

最直接的渲染管线是按照网格和光源进行遍历,依次把计算得到的颜色累加到指定的像素上。这样的渲染管线称为前向渲染(forward rendering)。

对于半透明的物体,前向渲染还需要考虑物体之间的渲染顺序。比如说我们需要先绘制不透明的物体然后才能绘制半透明的物体,而在绘制不同的半透明物体时还需要按照深度由远及近进行绘制。

Deferred Rendering
前向渲染的一个缺陷在于当场景中的光源数比较多时效率会非常低下。为了克服前向渲染这样的问题,人们还开发出了延迟渲染(deferred rendering)的技术。在延迟渲染的管线中,每个像素的渲染分为两个步骤:首先把每个像素对应的各种几何信息存储到G-buffer中,然后再对像素进行渲染。

延迟渲染的优势在于它可以极大地提升渲染效率,而且G-buffer中存储的几何信息也可以方便后处理的各种计算;但它的缺点也很明显,延迟渲染需要首先将几何信息写入G-buffer中因此对显存有着更高的需求。

Tile-based Rendering
为了缓解延迟渲染的显存压力,我们可以使用分片渲染(tile-based rendering)的方式将整个画面拆成若干个分片,然后在每个分片上单独使用延迟渲染。这样的方式可以有效减少显存的需求,在很多移动设备上都有应用。

更进一步,我们可以把每个分片需要考虑的光源信息写入G-buffer中。这样在对每个分片进行渲染时只考虑可能出现在该分片上的光照,从而进一步提升渲染效率。

除此之外,我们还可以利用空间深度信息来进一步优化每一个分片上需要考虑的光源。这样可以更高效地处理多光源的场景。

很多游戏还是要了forward+ rendering这种形式的渲染管线,它是将forward rendering按照分片进行渲染的绘制方式。

Cluster-based Rendering
对空间进行划分时还可以同时按照深度进行切分,这样可以处理场景中有上千个光源的极端情况。它可以看做是分片渲染在深度上的推广。

Visibility Buffer
目前游戏工业还发展出了基于V-buffer的渲染管线。V-buffer与G-buffer类似,但在V-buffer中储存的是像素的深度和面片信息,而在实际渲染时根据每个像素对应的几何信息来进行绘制。这样的渲染管线更加适合如今越来越复杂的几何场景。

Frame Graph
在商业游戏引擎的渲染系统中,除了需要包含各种先进的渲染算法外还需要考虑如何对各种算法有序地进行管理。以虚幻引擎为例,整个渲染管线中包含了大量的可选算法,在实际渲染时需要进行相应的调度使它们按顺序进行执行。

同时,像Vulkan和DirectX 12等现代图形API往往开放了大量的GPU底层接口进行编程。这使得程序员可以高效地实现对硬件计算资源的管理,但相应的如果开发时不够谨慎则容易造成整个系统的崩溃。

为了便于整个开发和渲染流程,游戏工业目前尝试使用frame graph这样的技术对整个渲染过程进行管理。它的思想是把整个渲染过程所需的算法和各种资源表示为一张有向无环图(DAG),这样就可以通过对图的管理来实现不同资源的调度。当然frame graph这样的技术仍在探索阶段,但我们可以期待它在今后整个游戏工业界的表现。

Render to Monitor
当我们在GPU完成渲染后就需要把渲染的结果输出到显示屏幕上。这里需要注意的是当显示器的刷新频率和GPU的输出频率不一致时会产生画面割裂的情况。


为了克服这样的问题人们开发出了V-Sync技术,它会强制显示器等待GPU的输出结果这样就可以避免画面的不一致。当然V-Sync也带来了一些新的问题:在场景发生变化时画面的帧率会忽快忽慢影响玩家的游戏体验。

因此目前一些硬件厂商提出了variable refresh rate的概念,让显示器画面的刷新率可以由GPU根据需要来动态设置。
