GAMES104课程笔记06-The Challenges and Fun of Rendering the Beautiful Mother Nature
这个系列是GAMES104-现代游戏引擎:从入门到实践(GAMES 104: Modern Game Engine-Theory and Practice)的同步课程笔记。本课程会介绍现代游戏引擎所涉及的系统架构、技术点以及引擎系统相关的知识。本节课主要介绍现代游戏引擎中的地形和大气渲染技术。
现实世界中有着丰富的自然场景,如果只使用简单的绘制程序则很难给予玩家真实的游戏体验。因此在本节课中我们会介绍3A游戏中使用的自然场景渲染技术。


Landscape
目前的3A游戏中以及可以生成逼真的地形环境渲染效果。以微软的模拟飞行为例,最新一代的模拟飞行已经基本实现了真实地球的地貌绘制,此外基于地形绘制技术我们也可以生成其它星球的地形和地貌。


Terrain Geometry
Heightfield
表示地形最简单的方法是使用高度场(heightfield)。我们可以把地形看做是平面上具有不同高度的函数,然后通过在平面进行均匀采样来近似它。这种方法在遥感等领域仍然有着很多的应用。

高度场的缺陷在于当我们需要表示大规模的地形或者需要更精细的地形时所需的采样点数会成倍的增长。

在游戏引擎中由于玩家观察的视野(field of view, FOV)是有限的,实际上我们不需要对所有的网格进行加密采样,只需关注视野中的地形即可。在这种思想下人们提出了两条加密采样原则:首先是根据距离和视野来调整网格的疏密,对于不再视野范围内或是距离观察点比较遥远位置的地形无需使用加密的网格;另一条是在对地形进行采样时要考虑对网格进行加密或者化简后地形高度的误差不要超过一定的范围,我们希望近处地形的误差尽可能小而远处的误差可以大一些。


对三角网格进行加密可以通过三角网剖分算法来实现。对于均匀分布的网格,其中每个三角形都是等腰直角三角形。因此在进行剖分时可以直接选择三角形的斜边中点将它剖分成两个一样的小等腰直角三角形。显然这样的剖分方法等价于为二叉树添加叶节点。

在进行剖分时还需要注意T-junction的问题:当我们对某个三角形进行剖分后必须同时将与它具有相同邻边的三角形同时进行剖分,否则会有顶点落在其它三角形的边上。

利用现代GPU的计算性能和上面介绍的三角剖分算法就可以实现大规模场景地形的实时渲染。

当然在游戏行业中更常用的高度场表达方式是使用四叉树来表达地形。这种方法更符合人的直觉,同时也可以直接使用纹理的存储方式来存储这种四叉树的结构。


quad-tree同样需要考虑T-junction的问题。不过在quad-tree中可以通过将三角形顶点之间吸附到其它顶点上的方法来简化处理。

Triangulated Irregular Network
在很多场景中均匀采样的地形会造成一些存储空间的浪费。实际上对于高度变化不大的区域只需要少量的三角形就可以进行表达,而对于高程变化剧烈的区域使用数量更多的三角形来还原地形的细节。

不过目前这样的方法在游戏工业界的应用还比较少,主流的地形处理方法仍然是使用均匀采样的网格。

Hardware Tessellation
利用现代GPU的强大计算能力我们可以把地形的细化完全放到GPU上进行实时计算。在DirectX 11中提供了hull shader、tessellator以及domain shader等工具来网格的实时细分。


在更新的DirectX 12中则将这些概念合并到mesh shader中,通过mesh shader来实现全部的网格细分功能,从而极大地方便来游戏开发和图形程序。

此外还可以利用GPU的计算性能实现动态的地形绘制,从而进一步提升玩家的游戏体验。

Non-Heightfield Terrain
有些游戏场景如洞穴可能无法使用高度场来进行表示。

对于这种场景可以考虑使用体素来表达场景的几何,然后利用marching cube算法来生成表面。


当然这种方法目前仍处于试验阶段,几乎没有游戏使用相关的技术来表示地形。

Terrain Texture
Texture Splatting
有了地形的几何表示后就可以为它添加纹理细节进行渲染。

基于纹理合成算法,我们可以控制不同纹理之间的混合比例从而获得接近真实地形的纹理。

此外我们还可以利用地形的高程来动态调节混合比例,从而实现高低起伏上不同的纹理效果。


当需要对多种不同材质进行混合时还可以使用texture array来管理不同材质的混合关系。

除此之外在地形渲染中还大量使用了视差贴图的技术来产生立体感。

Virtual Texture
直接对地形纹理进行混合时容易造成计算上的性能问题,这是由于对纹理进行插值的计算是相对复杂的。如果没有设计好渲染管线则会导致渲染效率的下降。

在现代游戏引擎中大量使用了虚拟纹理(virtual texture)的技术来提高渲染性能。使用虚拟纹理时首先需要把纹理分解成若干个尺寸相同的tile,然后把不同LOD的纹理则需要事先进行烘焙存储在硬盘上。在实际渲染时根据绘制目标的精度来决定所需的LOD以及对应的tile,然后将需要进行渲染的纹理tile加载到内存中作为实际的纹理贴图。这样的方式可以极大地缓解纹理读写的内存需求从而提高渲染效率。

显然虚拟纹理的性能瓶颈在于从硬盘加载纹理的IO过程。想要进一步提高效率甚至可以直接让GPU从硬盘进行加载。

Camera-Relative Rendering
当渲染物体与相机的距离达到一定程度时就需要考虑浮点数的计算精度问题,如果不进行处理会导致严重的抖动和穿模现象。

想要缓解这种问题可以将相机设置为世界坐标的中心,这样的处理方法称为camera-relative rendering。

Decorator
除了各种地貌纹理外,游戏设计中还需要在地面上设置各种植被、草丛、道路等各种装饰件(decorator)。这些装饰件看起来很简单,但实际上要想做出比较好的效果仍然需要非常复杂的算法。



Sky and Atmosphere
Atmosphere
Analytic Atmosphere Appearance Modeling
对于天空和大气的渲染,最简单的方法是使用拟合公式来直接计算着色。当然这种方法的缺陷也很多,比如说它只能表示从地表进行观察的效果,而且可以调整的参数也有很多的限制。

Volume Rendering Equation
如果想要渲染出真实的大气效果则需要考虑光照与大气相互作用的物理过程。

当光线与大气中的各种气体分子以及灰尘等气溶胶发生相互作用时有三种可能的现象:吸收(absorption)、发射(emission)以及散射(scattering),其中散射又可以分为内散射(in-scattering)和外散射(out-scattering)。基于辐射传输方程(radiative transfer equation, RTE)我们可以得到出射光线的radiance在指定方向上的微分。

通过对RTE沿光路进行积分,我们可以得到光线穿越介质后的radiance。这个方程也称为体渲染方程(volume rendering equation, VRE)。

Scattering
求解VRE最复杂的地方在于如何计算散射项。在大气渲染时我们一般只考虑Rayleigh散射(Rayleigh scattering)和Mie散射(Miew scattering)两种形式的散射。当大气中介质的尺寸远小于光线的波长时会发生Rayleigh散射,此时散射自身是无方向的而且散射的行为只与光线的波长有关。


当光线发生Rayleigh散射时太阳光中不同波长的色光会发生不同程度的散射。具体而言短波长的蓝光会出现大量的散射行为,而长波长的红光则只会发生少量的散射。这样的现象就导致了白天我们观察天空时眼睛会接收到来自四面八方散射的蓝光,因此天空呈蓝色;而在日出或是傍晚时太阳方向上只剩下未散射的红光,此时太阳呈红色。


当大气中介质的尺寸接近或大于光线的波长时会发生Mie散射,它的特点是散射程度与波长无关只与观测方向有关。


我们日常生活中常见的雾气和光晕都是Mie散射的结果。

Absorption
除了散射外,在大气渲染时还需要注意不同的气体分子对于不同波长的光线有着不同的吸收行为。

Multi Scattering
在计算散射时还要注意大气的多重散射行为:在积分时不仅要考虑光路上介质的散射行为,整个空间中介质粒子都会对接收到的光线产生贡献。


Ray Marching
对大气进行渲染时可以利用ray marching算法进行计算,它的思想非常简单:我们只需要把整条光线分解成N段然后在每一小段上单独进行积分,最后把N段的积分相加即可。

而在游戏引擎中更是可以通过预计算的方式提前存储在硬盘中,这样实际渲染时只需要进行查表即可。以透射率为例,我们可以把大气的透射率分布参数化为海拔高度与天顶角的函数,然后通过预计算存储在一张纹理图像上。

对于单次散射的情况,我们同样通过预计算的方法将散射参数化为海拔高度、观测角度以及太阳角度的函数。

最后我们把透射率的纹理图像以及单次散射的纹理不断进行积分就得到了多次散射情况下大气渲染的预计算纹理。目前很多3A游戏的天空渲染都是基于这样的方式进行处理的。


上面介绍的方法虽然可以获得非常好的效果,但它仍然具有一些缺陷:比如说离线的预计算仍然是非常费时的,而且在实时渲染时高维纹理的插值对于一些移动设备不够友好,更重要的是它很难处理大气成分发生变化时的渲染问题。

为了缓解这些问题,人们还开发出了各种近似方法。比如说我们可以假设大气是各向同性的均匀介质,然后使用一个衰减系数来模拟单次散射后接收到的光线。这样可以使用几何级数来表示光线经过无限次散射后到达相机的能量比例。

对于高维纹理的问题可以假设观察位置和太阳位置是不变的,这样光照就只是观测方向的纹理。

最后通过ray marching沿路径进行积分就可以得到大气散射的近似效果。

实践证明这样的近似方法在一些移动设备上也可以实现不错的渲染效果。

Cloud
在大气的基础上添加云可以实现更加真实的环境渲染效果。

早期对云进行渲染的方法是使用网格来建立云的模型。这种方法可以渲染出高质量的云,但由于它非常不灵活现在基本已经弃用。

过去也出现过使用透明通道来近似云效果的方法,不过这种方法很难生成逼真的渲染效果。

目前3A游戏一般会使用体积云的方式来对云进行渲染,尽管它有着比较高的计算复杂度但却可以实现逼真的渲染效果。

使用体积云进行渲染时我们需要一张weather texture来表示云在平面以及厚度的分布。

生成体积云模型时首先使用weather texture产生柱状的云层,然后利用Perlin噪声和Worley噪声进行腐蚀就可以产生逼真的云模型。


进行渲染时使用ray marching的方式来计算散射就可以实现逼真的渲染效果。
