《巅峰极速》负责人周潜:赛车游戏如何在移动端实现更逼真的光照效果?
2022N.GAME网易游戏开发者峰会(以下简称峰会)于「4月18日-4月21日」举办,本届峰会围绕全新主题“未来已来 THE FUTURE IS NOW”,共设置创意趋势场、技术驱动场、艺术打磨场以及价值探索场四个场次,邀请了20位海内外重磅嘉宾共享行业研发经验、前沿研究成果和未来发展趋势。
在第二天的技术驱动论坛上,网易游戏大话事业部高级技术经理周潜带来了题为《极速光影——探索赛车游戏的光照》的演讲,以《巅峰极速》为例,介绍如何针对赛车游戏特点在移动端实现的多光源方案以及实时环境的全局光照方案。
以下是演讲实录:
周潜:大家好,我是来自网易游戏大话事业部的技术专家周潜,那么我今天邀请大家跟我一起走进一个极速光影的世界,来把我们在高品质赛车游戏的一个光照方案分享给大家。
首先来介绍一下我们的游戏产品,我们的游戏名字叫《Racing Master》,中文名叫《巅峰极速》。我们的目标是打造一个高品质、高拟真、高画质的赛车游戏。
这是我们在游戏内实录的场景,可以看到游戏场景非常丰富,并且色彩也比较鲜艳。
这是我们的开场动画,赛车运动系统(包括移动、漂移、加速等)通过物理写实和物理计算等技术,达到极致拟真效果。
玩家可以对车辆进行非常丰富的自定义,包括改装、涂装等等。
在大厅界面具有车辆展示系统,可欣赏建模非常精致的赛车,包括极高还原度的车灯、车漆,以及在夜景下十分写实的全局光照。
通过以上介绍,相信大家也对我们一个游戏的美术品质有了一个大概的概念了,为了达到这样的美术品质,我们面临最大的一个难点,就是“实时全局光照”。
而全局光照分为两个部分:直接光照和间接光照。为了在移动端达到这个目标,我们面临非常多的挑战。包括带宽、性能、以及兼容性等。
还有很多的束缚,比如在减少DP和Pass之余,需要用到Forward管线来减少宽带。
那么,直接光照和间接光照在游戏里延伸出来两个需要解决的问题,就是“实时多光源”和“实时环境捕捉”,我们今天的话题就从这两个问题开始。
一、实时多光源
首先是实时多光源。“多光源”在我们游戏里是非常常见的,比如说在夜景下,会有许多路灯、车灯以及车辆的回火等等,它们会照亮周围的物体。
首先来看一个效果演示,这个是我们赛车在经过一排路灯下的表现,除了路灯之外,车辆还有一个前置的车灯,众多的动态光源在Forward管线下是难以实现的。
因此,许多人对Forward管线进行了改进,以此来支持多光源。这里介绍两种方案,分别叫Tile Shading(又叫Forward+)和Clustered Shading。
首先是Tile Shading,它的思路是把屏幕空间划分为多个格子,然后每个格子配合深度Buffer去进行灯光求交计算。这样我们就能知道每一个格子里会有哪些灯光对其有影响,从而减少每个像素需要计算的灯光数量。
它虽然能够解决多光源的问题,但需要一个预绘制深度的Buffer,即一个PreZ Pass。但如果当游戏场景非常丰富时,PreZ Pass便会带来非常多的Draw Call,这对于我们来说是不能接受的。
此外,Tile Shading的求交计算必须得在Compute Shader里面进行,可对于许多手机而言,关于Compute Shader的支持效果并不友好。
接着是Clustered Shading,它的思路是在Forward+的基础之上,对深度空间进一步地划分,将视锥体划分为多个视锥体分块,之后再对每个分块进行灯光求交计算。
但同样的,Clustered Shading也需要一个PreZ Pass,并且它的求交计算也需要放在Compute Shader当中去进行。
为了在移动端去解决这些问题,我们就提出来一种新的多光源的方案,叫做:“Grid Shading”。
Grid Shading的思路是这样的:首先是在世界空间上,沿着XY轴方向进行齐轴的对齐网格划分;然后采用一张灯光索引图,其中每个像素代表一个格子且包含该格子内所受到的光源编号。
每个像素用RGBA四个通道,可以记录四盏灯光。如果超过了四盏灯光怎么办?那就需要对这些灯光进行贡献度排序。
贡献度,即灯光的光照强度。在排序之后只保留贡献度最大的四盏灯光于这个像素当中。而灯光信息则通过UniformBuffer上传。
此外,我们可以在Z轴方向进行多层扩展,这样就可以达到覆盖范围更广的目的。
通过Grid Shading在场景中的应用可以看到,场景里产生了一个XY方向上64×64的网格,并且我们在摄像机水平高度方向上,向上向下各扩展两层。因此产生的总网格大小是64x64x4。
但由于网格是扁平形状的,所以它只能覆盖较为水平的范围,并且使用这种方案时,玩家的视野范围也必须受到限制,得在水平方向上的视野。
可在赛车游戏里,赛道基本是平铺的,且玩家的视野基本处于水平方向,所以需要照亮的场景物体,包括赛车、路面等等都正好在覆盖范围之内。因而,产生的网格基本上能满足我们的需求。
那Grid Shading的流程是怎样的呢?
首先需要对灯光进行遍历,根据灯光的包围盒计算出格子的范围,然后在CPU层对每个格子进行求交计算。
接着计算出光照贡献度并进行排序,将结果填充到灯光索引图当中,并上传灯光信息。
最后,在绘制阶段,每个像素根据在GPU世界坐标的位置,计算出它所在的格子。并且在灯光索引图中,查找出它所需要计算的灯光编号,计算灯光的光照。
在这当中最难的一个点是格子的求交计算。每个格子的形状是立方体,可以把它近似成一个包围球。如果对方是点光源,则灯光范围恰好也是球体,球体与球体之间的求交计算非常简单。
但如果对方是聚光灯,聚光灯的范围是圆锥体,那么该如何做圆锥体与球体的求交计算呢?通常情况下,我们一般是提取出圆锥的包围球,然后将该包围球与格子的包围球进行求交计算。
虽然这种方法比较简单,但结果非常不精确,因为圆锥包围球的大小与圆锥本身的大小差异非常大。为此,就需要一种更精确计算圆锥和包围球的方法。思路如下。
首先,将圆锥的顶点置于一个大球的球心,则圆锥体的范围可以看作是该大球的一个球面角内。然后根据大球和格子包围球的位置关系,可以将求交情况划分为4种。
第一种情况非常简单,即格子包围球包含了圆锥体顶点,根据这种情况可以很清楚地看到包围球与圆锥体相交。
第二种情况是包围球与圆锥体大球相互分离,那么根据这种情况,则可以看到格子包围球与圆锥不可能相交。
剩下两种情况较为复杂。第三种情况是,格子包围球有不超过一半的体积在大球内部。那么此时就需要将相交的部分转化成一个球面角,接着用该球面角与圆锥体的球面角进行相交判断。
而第四种情况是,格子包围球有过半的体积在大球内部。那么我们可以用大球的球心,与格子包围球球面所构成的切线方向,来形成一个球面角。然后用该球面角与圆锥体的球面角进行相交判断。
通过以上四种情况的讨论,便可以很精确地判断出聚光灯和格子的求交情况了。
接下来,是Grid Shading在实际场景当中的应用。
右边这张图,是在比赛场景里放置的一个路灯,以及车辆本身的前置灯二者所构成的光照情况;左边这张图是关于该场景的灯光索引图。
怎么看这张索引图呢?通过观察发现,灯光索引图从上往下分为4层,对应到网格当中便是4层不同高度的网格。(前文提到的64x64x4网格的那“4”层)另外,每个像素是一个格子。
如果,该像素里有颜色,则代表这个格子受到了灯光影响。从索引图中可看到,出现的蓝色区域是聚光灯所覆盖到的范围。该范围从上往下是逐渐增大的,那么也就对应了聚光灯上小下大的圆锥体形状。证明索引图的结果与场景完全对应。
因此,我们在实际绘制时,就可以采样这张索引图来判断像素所对应的灯光到底是哪些了。
最后我们来比较一下Grid Shading和剩下两种方案的区别。
首先在PreZ Pass阶段,对于Grid Shading而言是完全不需要该阶段的,而另外两种方案对该阶段则无法避免。这能够为我们节省很多Draw Call和Pass。
在求交计算方面,Grid Shading完全可以放在CPU层面去进行,并且计算过程非常简单,所得到的圆锥体相交结果也非常精确。但另外两种方案不仅无法放在CPU层进行计算,且计算过程比较复杂。在面对聚光灯时,计算结果也不够精确。
从划分颗粒度方面来看,Grid Shading是一个能够划分得非常精细的方案,Clustered Shading由于在Z轴上有更进一步的划分,因此相对来说也比较精细。但Tile Shading它是一个屏幕划分,所以划分的颗粒度非常粗。
所以可看到在以上几个方向上,Grid Shading非常有优势,并且在移动端上它的性能非常可以接受。就算在终端机上,求交计算过程中的开销也不超过1ms。
Grid Shading的问题在于,它需要一个平铺的水平视野范围。但对于赛车游戏而言,玩家的视野范围也正好是平铺的且处于水平方向。所以,这个限制对于我们来说并没有太大影响。
因此,Grid Shading可以说是一个非常适用于赛车游戏的多光源方案。
二、实时环境捕捉
讲完了多光源之后呢,我们来看看关于实时环境捕捉的研究。首先来看一个效果演示。
在演示场景中有非常多的高亮物体,比如烟花。那么可以看到,在夜晚或者灯光比较暗的场景下,这种高亮物体对场景的影响甚至比直接光照所带来的影响更大一点。
反应在演示中就是,赛车和路面是可以被烟花这种物体所照亮的,而且赛车同时还会受到路面以及周围物体反弹的间接光影响。那么为了达成这种环境的光照效果,最重要的一步就是实时环境捕捉。
在移动端通常采用的是一种叫做“双抛物面映射”的方案。
它的思路是,将360°的环境通过两个抛物面映射到上和下两个方向上,通过两张图来表达整个场景的信息。
右边这张图就是我们游戏里面的捕捉到的两张双抛物面贴图,赛车和赛道都必须要去采样这两张贴图来来获取环境信息。
为什么要采用这种双抛物面的映射方案呢?以下是我们对于几种不同环境捕捉方案的比较,相信通过比较便能得出结果。
一般来说全场景捕捉有三种方案,分别是“球面映射”、“立方体映射”和“双抛物面映射”。
在渲染目标数方面看,三者渲染的目标分别是1张、6张、2张。渲染数目越多代表需要更多的Pass以及Draw Call。
在畸变层面来说,球面映射会有一个非常大的畸变,且越是在边缘处畸变越大;立方体映射完全不存在畸变,双抛物面映射虽然也有畸变,但比较小,是可以接受的。
从映射质量来说,球面映射在边缘处的映射质量非常差,并且有奇点;立方体映射的信息量最大,所以映射质量是最高的;而双抛物面映射的映射质量一般,但在移动端仍可以接受。
从计算复杂度上看,我们需要对顶点做映射变换。因此球面映射的映射变换最为复杂,因为它需要用到开方运算;而立方体映射由于只是一个简单的透视映射,所以相对简单;同样,双抛物面映射的映射变换也比较简单。
那么,综合来看,双抛物面映射是非常适合用于移动端的环境捕捉方案。
在捕捉方向的选择上,我们可以选择前后捕捉、左右捕捉或者上下捕捉。如果选择前后捕捉或者左右捕捉这种方案,由于场景是平铺的,因而赛道在这种划分方向上会出现三角面的裁剪。最后在合成环境图时就会有裂缝,这对我们来说是不可以接受的。
如果采用上下方向去捕捉呢?虽然也有三角面的裁剪,但裁剪的位置非常远,玩家很难注意到。最后合成出来的环境光照也非常完整。
除了对捕捉方向的选择外,还需要对捕捉点的位置进行选择。
首先来看一张图,左边这个是湿滑路面的反射效果。大家可以看到这个路面反射有什么问题吗?很容易注意到的是,路面反射与实际场景的位置是不对应的。再来看这张图,右边这张图看起来就好多了。
两张图为什么会有这样的区别呢?
我们将相机位置给大家展示一下,左边这张图我们可以看到,朝前的相机是游戏视角相机,朝上和朝下的相机是环境捕捉相机。捕捉相机和游戏视角相机并不在同一个位置上,这就导致了画面中位置不对齐的现象。
我们可以看到右上角的示意图。当我们的捕捉点与相机在垂直方向上不一致时,它们在对于用一个反射方向,所捕捉到信息在横向上是有差异的。如果捕捉点与相机在同一个垂直方向上,那么捕捉到的信息只会在纵向上有一定差异,但在横向上是对齐的。
也可以看到左边这张图,虽然在纵向上有差异,但玩家很难注意到。可如果在横向上有差异,玩家就会非常容易观察到这个现象。
不过,这又带来一个新的问题,如果想要保证车辆的反射正确,就必须将捕捉相机的位置放在车的附近。但在游戏中,游戏视角相机与车辆本身的位置是有一定差异的。
所以我们没办法保证地面反射与车辆反射处于同时精准的状态,二者只能选其一。可是对于玩家而言,很难注意到车辆反射的不准确性,而是更容易注意到地面反射的不准确性。
因此我们会将捕捉相机与游戏视角相机放在同一位置,这样来确保地面反射的准确性。
在捕捉完场景之后,需要在IBL里面采样这两个捕捉内容来生成环境贴图。但IBL有一个要求,即在粗糙度比较高的情况下,它需要对环境贴图进行滤波。
原本我们直接对双抛物面的捕捉结果进行动态生成Mipmap,来近似这个滤波之后的环境贴图。但这样会带来一个问题,如下图。
图中是一个带有粗糙度的球体,它的上下半球之间有明显的分界线,这是怎么回事呢?
这是因为在捕捉时,朝上的半球受到的光照强度比较高,朝下的半球光照比较弱。在滤波时Mipmap只能对一张贴图进行Mipmap,没办法对整个环境进行混合。因此就带来了上下半球亮度不统一的现象。
怎样解决这个问题呢?我们又回想到了球面映射方案。因为球面映射是一整张贴图,所以对它生成Mipmap时,可以对全场进行滤波。于是,可以把双抛物面捕捉到的结果通过球面映射合成到一张贴图上。
此外,为了减少纹理的绑定及采样,还可以把双抛物面的两个捕捉内容分别放在同一个纹理的不同Mip上,这样就能减少一部分开销。
接下来,我们来看一下双抛物面捕捉的流程。
首先,为了减少Pass和Draw Call,会把上下半球的捕捉分成两个阶段进行。一帧捕捉上半球,一帧捕捉下半球,两帧交替进行。这样每一帧只需采样一个半球便可。
采样到环境之后,再把它用球面映射的方式合成到一张贴图上,接着再生成Mipmap。最后绘制阶段,把它应用到场景像素的绘制中。
那么,在室外场景下看,这样的表现是非常好的。但是,当我们把车开进隧道之后,又出现了一个新的问题。
如右边这张图,这是一辆白色的918,但在进入隧道后,它就变成一辆黑色的车了。这是为什么呢?原因是我们在隧道中捕捉的场景非常暗,它缺少静态光信息。
这时,就需要去获取静态光信息。那静态光信息到底存放在哪里呢?它存在于我们的光照探针里,所以就需要从光照探针里获取更多的信息来进行渲染。
一般来说,间接光分为Diffuse和Specular两个部分。Diffuse是在游戏里通过求些系数的光照探针来表示的。它是一个预烘焙后的包含静态光的信息。而Specular则是实时捕捉的内容。
在粗糙度变大的时候,需要把Specular向Diffuse的辐照度去靠拢。那么如何实现呢?UE4其实已经做了类似的流程,但那只是针对静态的一个方案。我们将这套流程进行了改进来适用于动态捕捉。
计算分为两阶段。首先在Vertex阶段,需要对这个球面映射的内容采取它最高级的Mips,并且将这个像素的内容点乘(0.3,0.59,0.11)。这是RGB各个通道的亮度权重值。
通过上面就得可以到环境的平均亮度。在Pixel阶段,用Diffuse辐照度除以平均亮度,并用粗糙度在1.0到刚刚计算出来的这个数值之间做插值。然后将这个插值乘以Specular,这样就能让Specular的亮度进行提高,达到跟环境一致的效果。
那么这样看来,在隧道中汽车原本的颜色也能回来了,并且它的效果也跟周围的环境比较统一。这就是全局光照的一个表现。
在讲完了实时环境捕捉以及实时多光源后,把这两者结合起来,看看在游戏里的实际效果。
最后,我们来做一个技术的展望。
我们这套方案可以用于实时环境光照变化比较剧烈的场景,对于光源高频变化具有较好的适应性。除了赛车游戏之外,还可以用于不同游戏类型,比如说MMORPG、FPS等等。
未来,我们还将会把这套方案延展到大世界以及昼夜变换和天气系统中。另外还计划玩家自定义赛道,这就意味着需要去实时捕捉环境间接光Diffuse的光照,这也是我们目前正在研究的一个方向。
这就是我演讲的全部内容了,谢谢大家!