上个时代的3A游戏,使用的是什么光照技术?| GAMES104实录 - 现代游戏引擎:从入门到实践

本期为GAMES104《现代游戏引擎:从入门到实践》视频公开课文字实录第16期。本课程由GAMES(图形学与混合现实研讨会)发起,游戏引擎技术专家王希携手游戏引擎一线开发者共同研发。

课程共计22个课时,将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。为配合学习实践,课程组在 GitHub 上开源了小引擎Piccolo,上线1个月即获得了2900+star, 累计下载量已超过20000+。

以下内容为公开课视频转文字版本,为阅读通顺,有删减

01「预计算的全局光照」

人类对自己的不满足也是推动技术不断进步的动力之一,很多从事3A游戏开发的人员都在不断突破自己,挑战自己,才使得游戏行业的技术不断进步,才使得大家能够体验到越来越优秀的3A大作。(获取本节完整课件,可公众号后台回复关键词:课件)

最近20年,特别是随着可编程硬件(Shader)的出现,游戏画质产生了突飞猛进的发展。特别是对于3A游戏行业来说,画面品质和一般游戏的差距越来越大。以《刺客信条》系列作品为例,第一代刺客信条游戏发布于2007年,当时的画面表现令很多人痴迷。在过去的十年中,随着《刺客信条·大革命》、《刺客信条·起源》、《刺客信条·英灵殿》的发布,游戏的画面表现也越来越好。今天我们介绍的仍然是上个时代的3A游戏效果,而并不是最近几年的新技术,因为最新的技术太复杂了,我们会在最后稍作介绍。

下面我们介绍5到10年前游戏行业中所使用的技术。根据上文所描述的思路,我们对于光照、材质和阴影效果依次进行改进。

在之前的方案中,我们将光照简化成了环境光。随着硬件的发展,我们可以考虑使用更复杂的算法,对光照进行预计算,以得到更精确的光照信息。因为对于游戏场景来说,90%的物体都不会移动,而太阳的角度也是固定的,因此我们可以通过空间换时间的思路,预先计算出光照信息。

大家可能听说过全局光照(Global Illumination)。对于来自四面八方的光照,我们可以将其分解为直接光照和间接光照。我们需要将这两种光照效果都预先计算出来。首先我们解释一下全局光照为什么这么重要:

上图中是一个白天的室内场景,如果室内没有灯光,并且我们只计算直接光照,我们看到的就是左图中的效果。我们可以通过一些技巧进行处理,比如给场景加上全局环境光,但这样会导致场景会统一变亮,看上去非常具有平面感。如果大家回想一下15-20年前的游戏,特别是一些网游,当进入到室内场景时,就会感觉室内场景特别有平面感,而几乎感受不到什么光照效果。这是因为这些游戏没有加入全局光照算法,只有全局光照才能模拟出这种光照效果。即光线照射到地面上,再从地面上反弹出来,又将天花板和墙壁照亮。这样的画面才具有真实感。因此,全局光照对于这种真实感的刻画非常重要。

扫描二维码关注公众号,回复: 14722685 查看本文章

这时我们的挑战是什么呢?我们所得到的全局光照信息,实际上是一个在球面空间的采样。我们可以将球面像地图一样展开成一个贴图。然而,这种贴图的数据量非常大,即使我们可以保存整个场景的光照信息,当我们为材质计算光照信息时,仍然需要进行积分运算。当然,我们可以使用最粗暴(比如蒙特卡罗)的方法,在球面上撒无数个采样点,依次计算每个采样点的光照信息,以进行近似。在绘制时,对于屏幕上的每个像素点,当对其进行着色时,我们都需要对这个球状的光照贴图进行采样,然后再进行累加。这实际上是一个卷积运算,计算量十分巨大。因此,我们需要一种方法,能够实现快速的积分运算。这就是摆在我们面前的挑战。

下面我们回顾一下GAMES101和GAMES202中的相关内容(编者注:信号与系统和傅里叶变换)。傅里叶变换可以将一个连续信号分解成多个不同频率的周期信号的累加。而全局光照信息可以看作是空间上的一个连续信号,我们可以通过傅里叶变换将其变换成频域中的多个不同频率信号的累加。然后截取前几项(越往后的项影响越小),就完成了对空间中的信号的近似表达。如下图所示:

虽然这是一个近似表达,但仍然可以辨识图中的内容。比如下图中只取了九项,就能够恢复出大致的图片。这样就可以节省很多存储空间,同时也有数学上的理论支撑。

傅里叶变换具有两个优点。第一个优点是可以高效地压缩数据。比如上图中的图片,分辨率大约是200×200像素,有四五万个像素点。然而,我们使用9个参数就可以大致表达整幅图片的信息。当然,真正需要的参数可能会更多。

第二个优点是可以降低卷积运算的复杂度。当我们对上图中的图像和另外一个图像做卷积运算时,我们并不需要对这几万个数据点依次进行乘法和累加运算,而只需要将其投影到频域空间的那几个参数进行一次卷积,就可以得到它的结果。这是一个非常有用的数学性质。所以当大家将这个性质提取出来之后,就得到了著名的球面调和函数(Spherical Harmonics,也叫做球谐函数)。将球面调和函数引入图形学领域的是微软研究院的Peter-Pike Sloan(下文简称P.P.Sloan)

在2002年发表的“Precomputed Radiance Transfer for Real-Time Rendering in Dynamic, Low-Frequency Lighting Environments”一文中,P.P.Sloan提出,可以对辐射亮度的传输进行预计算,以实现低频光照环境下的软阴影、互反射和焦散效果。为此,P.P.Sloan引入了球面调和函数。球面调和函数是球面上的一组正交基,类似于一维圆上的傅里叶级数(编者注:请参考GAMES202第6讲)。如上图所示。因为球面调和函数的基函数是相互正交的,因此定义在球面上的任意标量函数都可以通过积分计算出其在对应基函数上的投影系数。而通过这些系数,就可以重建出对应的近似函数。更有意思的一个属性是,球面调和函数的二阶导永远是零,这表示这个函数永远是光滑的。

在对光照信号重建时,我们就可以使用简单的多项式乘法和加法(即若干阶球面调和函数的线性组合),将这个信号大致表达出来。在游戏中,一般使用二阶到三阶球面调和函数就已经足够,如果只需要表示低频信号,使用一阶球面调和函数即可,因为我们只需要表达低频信息。请看下图中的示例:

在上图中,对于场景中的任意一点来说,每个点都有一个探针(Probe),记录了该点的Irradiance,我们将每个点的Irradiance信息展开,如右下图所示。然后使用球面调和函数对其进行压缩。随后在程序中对Irradiance进行重建,如果我们只使用二阶球面调和函数,我们会发现,虽然重建后的画面很模糊,但已经基本反映出了光照的方向信息,而且整个数据都很连续。如果我们想知道任意一个方向上的光强,只需要一次线性的向量运算即可。

上图选自2018年的Dice工作室的一个报告,报告中介绍道,在寒霜引擎中,使用了2阶球面调和函数的信息就已经足够。因为使用球面调和函数是为了表达环境光,而如果移除主光照信息,在很多情况下,环境光就是低频的,所以这样压缩的效果基本满足实际需要。当表达一个点处的整体环境光时,我们只需要12个参数。因为需要为RGB的每种颜色的系数分别进行压缩。

下面介绍一个细节。2阶球面调和函数虽然有四个参数,但这四个参数的权重是不一样的。对于L0来说,相当于对整个环境的光场做了一次加权平均。而现代游戏基本上是HDR模式的,所以对球面调和函数的系数的压缩采取了HDR压缩方法,这里可以选择BC6H纹理进行存储,H代表高分辨率,我们只需要使用Alpha通道即可。而对于L1阶的系数来说,则可以使用LDR压缩方法。即使使用高精度的BC7算法进行压缩,整个球面上的光照表达也只需要32个比特位。这相当于我们使用了一个和带有Alpha颜色的存储空间相同大小的空间,就能够存储每个点上的光场信息。第二个好处是,当使用球面调和函数表示光场信息时,在计算漫反射或镜面反射项时,都可以将其转换为两个向量之间的点积或者卷积。在Dice的论文中,只计算了漫反射,而在《Halo 3》中,还计算了镜面反射,因为《Halo 3》中存储的球面调和函数的阶数较高。大家如果有兴趣,可以自己推导一下。还需要注意的一点是,大家在自己实现球面调和函数时,需要对有些参数进行归一化处理,因为有时候如果忘记除以π,所得到的数值就不正确了。球面调和函数方法的主要思想是,在球面上的两个函数的卷积可以简化成将两个函数分别投影到球面调和函数上,然后对投影得到的结果进行卷积。

有了这个数学工具之后,我们就可以实现一些预计算效果了,这就是光照贴图(Lightmap)Lightmap最早并不是为了实现全局光照而发明出来的,如果没有记错,应该是John Carmark在开发《Quake》系列游戏时,由于硬件无法实时计算阴影,所以引入了光照贴图对阴影进行预计算。后来随着阴影技术的发展,这种做法慢慢被淘汰。然而,当大家开始考虑如何实现实时光照效果时,又受到了Lightmap的启发,通过球面调和函数,就可以将整个场景的光照信息烘培到一张图上。

通过上述方式烘焙出来的贴图称为Atlas,翻译成中文叫做航海图或者图集,即将很多几何体平铺到一张图上。首先,需要对世界的几何信息进行简化,不能使用非常精细的几何体。因为过程中有一步称为参数化(Paramitization),会将三维空间中复杂的几何体投影到二维空间(编者注:具体可以参考刘利刚老师等人的《GAMES301:曲面参数化》课程)。对于简单的几何体来说(比如立方体或球体),参数化是比较简单的。而对于任意形状的物体来说,参数化是比较复杂的,因此需要对几何体进行简化,并在参数空间中进行分配。这里有个细节需要注意,即我们希望对于同样的面积或体积,所分配的纹理的精度基本相同。

图中将每个纹素都标记成了不同的颜色,如果将该图反向投影回场景,每个格子的大小都比较均匀。这是光照贴图的一个很重要的要求。这个要求实现起来并不简单。DirectX在十几年前专门开发了一个API来帮助开发人员进行参数化展开,当时还存在不少问题。

下面,我们就可以进行光照计算了。这是通过很多台高配置的电脑所组成的集群来进行计算的,也叫做“Light Farm”。美术制作完场景之后,将光源等参数设置好,然后就可以开始烘焙。烘培过程一般需要半个小时到一个小时左右。这项技术会让3A游戏的开发效率急剧下降,后来我们自己开发引擎时,由于这个原因,没有应用光照贴图技术。当然,这样的效果会比应用了光照贴图技术的效果差,因为光照贴图烘焙出来的效果确实很棒。它可以烘焙出非常漂亮的全局光照效果,再叠加上直接光照,基本上可以实现光线的反弹,以及相邻建筑物之间的软阴影。这时再加上主光源,整个的光照环境就非常饱满。再配合上材质的效果,你会发现这种感觉非常真实。

如果场景中没有光照贴图效果,看上去就会非常难看。因为光有时只能照亮场景中的很小一部分,就算使用环境光项进行补光,对于那些不能被光直接照射到的结构来说,也无法看出其明暗和几何关系。所以全局光照就是全局自发光的感觉,对于3A游戏来说是非常重要的一个属性。我们不会详细介绍光照贴图,但是我想分享一下我个人理解的光照贴图的优势:虽然光照贴图的烘培速度特别慢,但运行时的效率非常高,因为运行时的成本很低。除此之外,由于光照贴图是离线烘焙的,在将整个空间进行分解之后,可以产生很多细节而微妙的效果。艺术家们非常喜欢这种效果。就像下图中的飞船,它在地上那种软软的明暗关系,会让你立即产生跃然于纸上的感觉。

光照贴图也存在着一定的缺点。第一个就是非常长的烘焙时间,我们在准备这一讲的课件时,无法找到一幅合适的光照贴图来让大家产生一个直观的印象,因为我们自己的引擎中没有使用这项技术,网络上也没有找到合适的资源。光照贴图的第二个问题是,它只能处理静态物体和静态光。当物体发生了移动,或者光源发生了变化,之前烘培的内容就无法继续使用了。当然也有研究人员在研究可以实时更新的光照贴图,但在工业界好像比较少,因为实时更新的技术难度很大。然而,对于动态物体来说,我们有一些技巧可以对其进行处理。对于一些简单的游戏来说,我们可以从物体周围去采样一个点处的光照贴图,然后可以猜测环境光的数值,从而给物体一个光照值。这样做也是有问题的,以前最经典的一个问题就是,地面上的光照贴图投射了一个阴影,角色走到这个位置时,这个阴影会将角色突然变黑,而当角色走开之后,又会突然变亮。第三个问题就是光照贴图的存储和GPU占用的消耗也很大。因为光照贴图本身是一个空间换时间的策略。在运行时,光照贴图本身可以轻松占用几十兆到一百兆的存储空间,具体取决于场景的大小。场景越大,烘焙时间也会越长。

在现代游戏中,光照贴图的方法可能会被逐渐淘汰。然而,光照贴图方案中所应用的两个思想值得大家借鉴。第一个思想就是空间换时间,光照贴图存储了大量的预计算结果。第二个思想是对场景的参数化思想。在光照贴图中,场景被参数化成了二维纹理,其实也可以参数化成三维的体素(Voxel)游戏中的场景本来是很复杂的,当我们将游戏场景变成一个易于管理和表达的二维纹理或者三维体素结构之后,就可以进行很多计算。希望大家能够理解这种处理方法。

在本门课程中,我们不会特别纠结于每个算法的具体实现细节。然而,我希望同学们在掌握了算法背后的思想之后,可以在自己的开发过程中进行举一反三。今天我们介绍的算法可能无法解决大家未来在从事引擎研发或者游戏研发时所遇到的其他问题。但是大家一旦掌握了这种思路之后,就可以发明自己的新算法来解决问题。

接下来,我们介绍一个简单粗暴的想法——光照探针(Light Probe)。在光照贴图的算法中,我们需要对场景进行参数化,将其“拍平”到二维空间上。这种参数化算法的实现非常复杂,也很容易出现各种各样的Bug。我们可以在空间上撒一堆采样点,叫做光照探针。每个点上的光照信息用一个探针来表示,我们可以在场景中布置一堆光照探针,如下图所示:

上图是微软的《FORZA:HORIZON》(微软的赛车游戏系列)游戏中的一个场景。游戏中使用了光照探针技术。图中可以看到,赛道上布置了多个光照探针。对于每个光照探针,采样它的整个光场,当物体或角色在场景中移动时,就可以利用物体周围的光照探针中存储的光照信息,进行插值,计算出物体上的光照信息。图中的小球在移动时,颜色一直在变化,这是因为随着小球的移动,它周围的光照探针中存储的光照信息也在变化(编者注:请点击阅读原文参考课程录像或者PPT中的动态演示)。这就是一个简单的空间体素化的算法。先将光照探针变成一个个的点,然后将这些点互相连接,形成很多四面体(Tetrahedron),随后就可以对其进行插值。

光照探针算法的实现难度并不大,但在具体的开发过程中,会面临一个问题——由谁来布置这些采样点?对于早期游戏来说,引擎开发人员一般会提供一个辅助工具,由艺术家使用辅助工具来布置采样点。然而,当关卡开发人员修改场景中的信息之后,艺术家就需要重新布置采样点。这样做一方面会导致效率低下,同时也会产生很多Bug。很多现代游戏都开始通过自动化生成工具来布置采样点。简单来说,首先对空间中的几何结构均匀地生成采样点,然后根据玩家的可到达区域(包括建筑物的几何结构),向外进行延拓,然后相对均匀地将采样点撒出去。对于如今的工业级应用来说,如果大家想实现光照探针功能,我建议大家实现一个光照探针自动生成的算法。因为在生产流程中,这样做会极大地节约艺术家的时间,避免了重复而枯燥的劳动。

在对光照探针进行采样时,采样密度会很大。为了避免占用过大的空间,我们会使用一些压缩算法来对光照信息进行压缩。因为我们只需要处理光照信息,而且大部分是漫反射信息。漫反射信息非常低频,我们可以使用球面调和函数对其进行压缩。

在游戏场景中,还有一些闪闪发光的物体,可以产生高频信息。当角色行走在这种环境中,所反射的信息就会经常变化。因此,我们有时会制作一种专门记录反射信息的探针,叫做反射探针(Reflection Probe)。反射探针数量不多,但采样精度很高。因为反射对于高频信息非常敏感,这样才能反射出周围的环境。反射探针一般会和光照探针分开采样。反射探针加上光照探针,就能够实现不错的全局光照效果。

反射探针和光照探针的运行时效率非常高,并且能够同时处理静态物体和动态物体。而且还可以在运行时对反射探针和光照探针进行更新。当场景中发生变化时,包括角色位置发生变化时,现代计算机上的光照探针是可以实时更新的。这里介绍一个光照探针的实现细节,在实现时,我们会在探针位置放置一个相机,然后分别向周围的六个位置(上下前后左右)观察,拍摄6张照片。将这6张图片拼成一个图,然后使用GPU的着色器对其进行处理。一般来说,在对光照探针进行更新时,我们不会每帧都对其进行更新,而会每隔几帧进行更新,或者当场景中的位置信息发生巨大变化时,才会进行更新。即使在这时,引擎也不会立即更新,而是会寻找一帧相对较空的时间进行更新。这也是现代引擎架构中所存在的一个理念,即一些可以延迟进行的更新可以延后进行,因为立即进行处理可能会导致帧率不稳定。

和光照贴图相比,光照探针可以给出同样的光照感,包括环境的效果。然而,对于光照贴图技术所能够实现的软阴影以及物体之间的交叠感来说,光照探针就无能为力了。同时,对于光的渗透效果(Color Bleeding)的实现,光照探针也不擅长。因为光照探针的采样比较稀疏,一张地图最多只有几万个采样点。而光照贴图的采样密度很高,一张地图上大概会采样几百万个点。光照探针的采样率大约只是光照贴图的1%的数量级,肯定无法达到和光照贴图相同的效果。

使用上述的光照贴图、以及光照探针的方法,在过去的十几年中,就能够近似实现一个看上去不错的光照效果。至此,我们就解决了光的问题。

本文编辑:Piccolo 社区编委会 彭渊

如对本节课有任何问题,欢迎加入我们的社群或给我们发送邮件:

[email protected]

关于我们

Piccolo游戏引擎社区

Piccolo社区是中国开源游戏引擎社区,由游戏引擎行业大佬、共创官、学习者共同建立。你可以在我们的社区里交流技术、互助问答、参加活动,你也可以参与Piccolo的共建,如撰写贡献代码、撰写技术文章、参与技术挑战等。

Piccolo游戏引擎

由中国游戏引擎社区Piccolo开源的一款Mini游戏引擎。采用世界-关卡-游戏对象-组件的简洁架构,便于理解游戏引擎架构思想,它不仅能有效的帮助开发者学习游戏引擎架构知识,也能帮助一线开发者实验引擎算法与第三方库、辅助个人项目快速启动。截止目前,Github点赞已突破3600+,累计下载量已超过20000+

Piccolo GitHub地址:https://github.com/BoomingTech/Piccolo/discussions

关注公众号GAMES104,回复【入群】,加入Piccolo社群
 

猜你喜欢

转载自blog.csdn.net/m0_74737520/article/details/129984647