2022 N.Game峰会 | 《巅峰极速》:极速光影——探索赛车游戏的光照

学习委员

2022-06-076454次浏览

0评论

3收藏

0点赞

分享

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”。

Grid Shading的思路是在世界空间上,沿着XY轴方向进行齐轴的对齐网格划分;然后采用一张灯光索引图,其中每个像素代表一个格子且包含该格子内所受到的光源编号。

每个像素用RGBA四个通道,可以记录四盏灯光。若灯光数目超出了四盏,则需要对这些灯光进行贡献度排序。

贡献度,即灯光的光照强度。在排序之后只保留贡献度最大的四盏灯光于这个像素当中。而灯光信息则通过UniformBuffer上传。

此外,我们可以在Z轴方向进行多层扩展,这样就可以达到覆盖范围更广的目的。

通过Grid Shading在场景中的应用可以看到,场景里产生了一个XY方向上64x64的网格,并且我们在摄像机水平高度方向上,向上向下各扩展两层。因此产生的总网格大小是64x64x4。

但由于网格是扁平形状的,所以它只能覆盖较为水平的范围,并且使用这种方案时,玩家的视野范围也必须受到限制,得在水平方向上的视野。

可在赛车游戏里,赛道基本是平铺的,且玩家的视野基本处于水平方向,所以需要照亮的场景物体,包括赛车、路面等等都正好在覆盖范围之内。因而,产生的网格基本上能满足我们的需求。

Grid Shading的流程

Grid Shading的流程如下。

首先需要对灯光进行遍历,根据灯光的包围盒计算出格子的范围,然后在CPU层对每个格子进行求交计算。

接着计算出光照贡献度并进行排序,将结果填充到灯光索引图当中,并上传灯光信息。

最后,在绘制阶段,每个像素根据在GPU世界坐标的位置,计算出它所在的格子。并且在灯光索引图中,查找出它所需要计算的灯光编号,计算灯光的光照。

在这当中最难的一个点是格子的求交计算。每个格子的形状是立方体,可以把它近似成一个包围球。如果对方是点光源,则灯光范围恰好也是球体,球体与球体之间的求交计算非常简单。

但如果对方是聚光灯,聚光灯的范围是圆锥体,那么该如何做圆锥体与球体的求交计算呢?通常情况下,我们一般是提取出圆锥的包围球,然后将该包围球与格子的包围球进行求交计算。

虽然这种比较简单,但结果非常不精确,因为圆锥包围球的大小与圆锥本身的大小差异非常大。为此,就需要一种更精确计算圆锥和包围球的方法。思路如下。

首先,将圆锥的顶点置于一个大球的球心,则圆锥体的范围可以看作是该大球的一个球面角内。然后根据大球和格子包围球的位置关系,可以将求交情况划分为4种。

第一种情况非常简单,即格子包围球包含了圆锥体顶点,根据这种情况可以很清楚地看到包围球与圆锥体相交。

第二种情况是包围球与圆锥体大球相互分离,那么根据这种情况,则可以看到格子包围球与圆锥不可能相交。

剩下两种情况较为复杂。第三种情况是,格子包围球有不超过一半的体积在大球内部。那么此时就需要将相交的部分转化成一个球面角,接着用该球面角与圆锥体的球面角进行相交判断。

而第四种情况是,格子包围球有过半的体积在大球内部。那么我们可以用大球的球心,与格子包围球球面所构成的切线方向,来形成一个球面角。然后用该球面角与圆锥体的球面角进行相交判断。

通过以上四种情况的讨论,便可以很精确地判断出聚光灯和格子的求交情况了。

Grid Shading的应用与对比

接下来,是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的光照,这也是我们目前正在研究的一个方向。

这就是我演讲的全部内容,谢谢大家!


视频版峰会回顾请戳:
https://www.bilibili.com/video/BV1oY4y1a7qM?spm_id_from=333.999.0.0

评论 0

0/1000
网易游学APP
为热爱赋能
扫描二维码下载APP