TGDC | Epic Games刘炜:虚幻5移动端延迟管线都有哪些新亮点?

GameLook报道/8月14日至8月17日,由腾讯游戏学堂举办的2022腾讯游戏开发者大会(Tencent Game Developers Conference,以下简称TGDC)将正式举行。

大会以Inspire Six Senses为主题,汇聚国内外顶尖游戏从业者以及学界专家学者,以激发游戏的创意力、想象力、洞察力、科技力、影响力和凝聚力,共同拓宽游戏产业的边界与可能性。

在8月16日的技术专场论坛上,Epic Games China技术支持工程师刘炜,进行了题为《虚幻引擎5移动端延迟管线》的分享。

以下是分享实录

刘炜:大家好,我叫刘炜,我是Epic Games中国的技术支持工程师,同时我也会参与虚幻引擎移动端的功能开发,很高兴能参加腾讯游戏开发者大会。

这次我主要想分享的是关于虚幻5移动端延迟渲染的相关内容,主要是想介绍一下虚幻5移动端延迟渲染的框架,所支持的功能遇到的兼容性问题以及优化方面的内容。

其实早在2018年左右就有开发者提出需求,希望我们支持移动端的延迟渲染,当时我们做了一个粗略的评估觉得是有可行性的,于是我们在虚幻4.26的版本里开始支持移动端的延迟渲染。

不过,情况比我们想象中的复杂,直到虚幻5.1的版本才算比较成熟,结果不算完美,但是过程是值得借鉴的。因为每次技术升级总会有失有得,希望这个过程所总结的经验,可以对大家在开发过程中遇到的问题有所启发,也希望这个技术可以更直接地帮助开发团队提升游戏品质。

这是我今天要讲的主要内容,第一部分是移动端延迟渲染,跟前向渲染做一个横向对比,看它们都有哪些不同,我们需要评估一下延迟渲染能给我们带来什么好处以及有什么限制。

第二部分是分析一下GBuffer的布局,以及怎样选择GBuffer的格式才能满足我们对功能需求的支持。

第三部分是介绍延迟渲染在虚幻4中有哪些所缺失的功能,在虚幻5.1中的实现情况,以及介绍一下它们的实现方式。

第四部分是介绍一下,我们遇到的哪些兼容性问题以及对应的修复方式。

最后介绍一下我们对于延迟渲染做了哪些针对性的优化。

其实延迟渲染技术已经相对比较成熟,没有太大的技术难度,所以这次我讲的内容,对于已经有这方面开发经验的程序估计没有太大的帮助,最多就是设备兼容性方面的问题可以留意一下。

不过对于那些对移动端延迟渲染感兴趣的团队,尤其是还在使用虚幻4.27的项目组,这次的内容可能更有意义,我会尽量把内容说得详细一点,方便大家参考,也希望大家找我多交流。

首先,我们主要从移动端比较关心的内存和性能方面,来比较一下延迟渲染跟前向渲染有哪些优势?又有哪些限制?

延迟渲染一个比较明显的好处是可以减少shader的变体,我们在Fortnite项目里一直想办法去减少shader的数量,因为项目比较大shader的数量比较多,每次更新的patch也比较大,对内存的占用也比较高。

我相信很多项目组也遇到过类似的问题,如果选用延迟渲染,base pass就不用计算光照了,这样光照计算的shader变体,就可以跟模型材质解耦开,就可以大量减少shader的数量。

这里我列了一下延迟渲染跟前向渲染各自所用到的shader变体,我们可以计算一下到底可以减少多少shader数量。在关掉静态光照的情况下,可以看到延迟渲染只有1种shader变体,而前向渲染有4种,算下来可以减少大概75%的shader数量,相当于只有原来的1/4大小。

再看一下打开静态光照的情况下,延迟渲染有4种shader变体,而前向渲染有18种相当于可以减少大约78%的shader数量,差不多是原来的1/5大小。

可以看到无论在哪种光照环境下,延迟渲染都能减少非常多的shader数量,相应的硬盘和内存也会减少很多占用,这是我们目前最大的收获,shader数量的减少不仅可以减小包体和内存的占用,还可以减少shader的编译时间,加快启动速度。另外,还可以减少游戏运行过程中的卡顿,因为很多卡顿可能是因为实时编译shader造成的,这也是延迟渲染给我们带来的额外的好处。

我们再来看一下延迟渲染对于性能有怎样影响,因为Base Pass没有了复杂的光照计算,所以变得更加轻量化了。Base Pass阶段的开销,应该会比以前减少很多,而且光照只在可见的像素上去做计算,不会浪费GPU时间在不可见的像素上,所以理论上应该也会减少GPU的压力,对性能提升是有帮助的。

不过现在的芯片大多都内置了减少OverDraw的技术,比如PowerVR GPU的Hidden Surface Removal和Arm GPU的Forward Pixel Kill,但是即便如此 ,在有些设备上还是能有明显的收益的。

另外就是使用延迟渲染可以有更多的Sampler给美术使用,因为这些Sampler原来在Base Pass里做光照计算用的,现在可以腾出来给美术用了。

大概数了一下,Reflection大概可以减少1至2个,Shadow texture可以减少1个,然后Planar reflection可以减少1个,Light grid data可以减少3个,所以在复杂的情况下,最多能减少6至7个sampler。

另外延迟渲染也没有占用更多的带宽,虽然延迟渲染需要创建多张GBuffer,但是我们可以利用Tiled Based GPU的特性把GBuffer分配在Tiled Memory上,这样就不用存回System Memory从而减少带宽占用。

不过这样也会带来一个问题,就是无法在后期中访问GBuffer里的数据,这也是目前移动端不支持Screen Space Reflection的一个原因。另外还有一些功能,是前向渲染没有的延迟渲染才支持的,第一个是带光照的延迟贴花,前向渲染的贴花是无光照的,延迟渲染因为有了GBuffer,我们可以在绘制贴花的时候改变GBuffer然后再计算光照,这样贴花就可以受光照的影响了。

另外,延迟渲染还支持IES profile和Light Function,前向渲染因为只支持Clustered Local Light所以不支持这两个功能。如果要支持的话会变得非常复杂。

另外延迟渲染的光照,默认会比前向渲染略微好一点,因为延迟渲染使用的是更准确的Environment BRDF,这个BRDF是bake在贴图里的,前向渲染因为要省一个sampler,所以用的是近似的版本还需要进行实时的计算,延迟渲染因为使用了这个版本只需要一次贴图采样,不需要实时地计算,所以性能上更好而且效果上也更加贴近PC。

不过我们在一开始支持不同硬件平台的时候遇到了一些问题,在对Mali设备做支持的时候发现mali的GPU不支持MRT的FrameBufferFetch,如果要支持OpenGLES就只能使用Pixel Local Storage的技术。这其实是Arm提供的另一种MRT FrameBufferFetch的形式,但是Pixel Local Storage有一个很大的限制,就是它的带宽只有128bit。

除此之外 我们还发现即便使用Vulkan RHI,在一些设备上查出来的最大支持的Input Attachment的数量只有4个,这种设备大概占所有Android设备的大概16.3%,为了支持这些设备,引擎只能控制Gbuffer的数量最多只能申请3张32bit的Render Target,并不能像PC上那样可以创建5张甚至更多的GBuffer来支持一些效果,这也是我们目前遇到的一个最大的问题。

接下来我们会想办法解决这个问题。关于抗锯齿,因为GBuffer已经把Tiled Memory都占用了,所以不能使用MSAA来抗锯齿了,引擎目前支持FXAA TAA,在虚幻5.1中我们也支持了FSR,虽然效率上肯定比不了硬件支持的MSAA,不过这种程序化的抗锯齿效果可能会更好。

另外我们也在考虑针对抗锯齿去做一些性能优化,比如给TAA增加一个PixelShader的版本,这样可以利用硬件的FrameBufferCompression来减少带宽占用。

现在我们已经知道了移动端延迟渲染的优势以及在移动端上的有哪些限制,接下来我们来看一下在这些限制条件下如何组织GBuffer的布局,才能满足移动渲染的功能需求。

首先在所有硬件平台上,我们都需要申请SceneColor和SceneDepth,SceneColor是始终要存下来的。SceneDepth可以根据情况来决定是否要存下来,如果后期用不到我们就不用存下来,这样可以节省带宽。

然后是GBuffer的创建,在Mali设备上如果是OpenGLES,GBuffer是在shader里,用Pixel Local Storage的方式创建在Tiled Memory上的,再加上32bit的SceneColor一共占用128bit,这也是Pixel Local Storage的一个限制。

如果是Vulkan RHI或者是在高通硬件平台上支持OpenGLES,GBuffer可以使用Memoryless的方式创建在Tiled Memory上,在使用Vulkan RHI的时候,GBuffer和SceneDepth都会算在Input attachment上,所以总共是不能超过4个,这也是现在很多移动设备的一个限制。

对于IOS设备,或者其它PowerVR GPU的Android设备,因为GPU不支持DepthBufferFetch,只能用MRT的FrameBufferFetch,所以只能新申请一张32bit的Render Target来存储Depth,这张Render Target是Memoryless的,所以不占用带宽,只是需要处理一下shader里FetchDetph的逻辑。

我们再来看一下GBuffer,要选择什么格式、以及存了什么信息。这张表是目前虚幻4里的结构,可以看到SceneColor里存的是间接光和自发光的信息,GBufferA是存了压缩过的法线和静态阴影的信息,法线因为需要比较高的精度,所以GBufferA用的是RGB10A2的格式。

GBufferB中存的是Metallic Specular Rougheness和ShadingModelID还有SelectiveOutputMask,GBufferC存的是Base Color以及Indirect Irradiance或者是Material AO。

这里我们可以得到一些信息,第一个是不支持延迟贴花的法线融合,因为法线是把3个通道的值压缩到2个通道里,所以混合后的数据是不对的。其它通道比如说Metallic Specular Roughness和BaseColor都是支持混合的。

第二个是虚幻4里不支持Shading Model只支持DefaultLit,如果要支持的话,正常情况下需要额外再申请一张GBuffer来存储Shading Model用到的Custom Data,但是因为硬件的限制没办法来新申请GBuffer,所以只能在现有的通道里面看一下有没有可以利用的PerObject Data和SelectiveOutputMask,在移动端上是没有用到的。

不过这个PerObject Data这个通道只有2bit,所以没法存储数据,Precomputed Shadow和Indirect Irradiance都是静态光照下才用到的,Material AO是动态光照下才用到的,然后ShadingModelID至少需要4bit才能表达所有Shading Model,所以我们可以考虑在这几个通道里面去想想办法。

这张表是我们根据前面得到的信息调整后的GBuffer结构,GBufferA就改用RGBA8格式,法线就不再压缩了,它占用3个通道,这样就可以支持延迟贴花的法线混合了。

但是8bit的法线精度可能还是不够的,比如说在一些反射比较好的效果下面,可能能看到一些瑕疵。另外如果要支持Shading Model,就需要腾出至少4个通道来存储Custom Data,这里Precomputed Shadow和Indirect Irradiance通道,因为只在静态光照下用到,在动态光照下就可以留给Cusom Data用了Material AO因为用的不多,也因为没有多余的通道了,所以有Shading Model的时候我们就不存这个值了。

另外Metallic在很多Shading Model里也是不用的,所以也可以用来存储Custom Data,还有一个Custom Data需要跟ShadingsModelID压缩在一个通道里,但是这样ShadingModelID的精度就不够了只有3bit,没办法表达所有ShadingModelID,而且5bit的Custom Data精度也不太够,所以这个方案也不是很完美。

这张表是现在虚幻5.1中GBuffer的结构,GBufferA还是用的RGBA RGB10A2的格式,法线还是压缩在两个通道里,虽然还是不支持延迟贴花的混合,但是法线可以有更高的精度,还可以多一个10bit的通道把ShadingModelID和Custom Data压缩在这个通道里,这样ShadingModelID就可以用4bit,CustomData可以用6bit,这样精度上也基本上足够了。

再加上Indirect Irradiance和Precomputed Shadow还有Metallic三个通道,就能存下4个通道的CustomData了,这样我们在关掉静态光照的情况下,就能支持Shading Model了。

接下来我们来看一下虚幻5.1中,支持了哪些新的功能。

第一个就是Shading Model,我们在关掉静态光照的情况下已经支持了所有的Shading Model,后面我会展示几个Shading Model的效果。

SingleLayerWater和Thin Translucent,这两个Shading Model是半透明效果才有的,延迟渲染就不用考虑它们了,因为它们是用前向渲染绘制的。

Eye Shading Model比较特殊一点,我在后面会介绍一下。另外还有两个Cvar是跟移动端Shading Model相关的,第一个是控制移动端是否支持PerPixel的Shading Model,支持的话一个材质球里,就可以有多个Shading Model效果,默认是开启的。

第二个是控制在移动端上,哪些Shading Model是打开的,如果没有打开就会用DefaultLit,这样可以提高效率,默认也是全部打开的。在开启静态光照的情况下,我们还是只支持DefaultLit。

不过最近我们在考虑去加一个选项可以保留GBuffer数据,因为像switch平台跑的就是移动端的渲染器,但是它不是Tiled Based GPU,所以GBuffer数据是始终会存下来的。既然它已经存下来了,我们就可以利用它去实现一些效果,比如去支持Screen Space Reflection,然后可以提高switch平台上的画面品质。

这是Eye Shading Model的效果,左边是原来DefaultLit的效果,中间是移动端上的效果,右边这张就是PC上的效果。Eye Shading Model有一点点特殊,它用到了Subsurface Profile Shading Model,所以在移动端上它用了5个Custom Data,我们就只能把其中两个Custom Data压缩到一个通道里,所以也许有可能你会看到,有些参数感觉精度不足,可能就是因为这个原因。

这个是Two Side Foliage Shading Model的效果,可以看到就是它比以前默认的效果会好很多。

这是Cloth Shading Model的效果。

这是Hair Shading Model的效果,这都是以前不支持,现在支持的Shading Model。

我们再看一下延迟贴花的一些效果,左边这张是前向渲染下的效果,右边这张是延迟渲染的效果。可以看到延迟渲染的贴花可以支持光照计算,这个效果会更加丰富,不过移动端不支持Dbuffer的贴花,因为这种贴花需要额外的带宽,对于移动端来说代价太大。

我们再来看一下光照和阴影支持的情况,延迟渲染大概分了这么几个pass。第一个是方向光的pass,这个pass里做了很多事情,它可以支持方向光的Light Function,它还支持Clustered Local Lights,还有Planar Reflection还支持CSM的Shadow还有Distance Field Shadow。

如果只有一个方向光,反射也是可以在这个pass里去一起计算,但如果有多个方向光,反射就会放在一个单独的pass里去计算,以免重复计算。

Local Light也有自己的pass,它也支持IES Profile Light Function,然后我们也支持了SpotLight Shadow,Local Light是分两次绘制的。第一次是在stencil去标记灯光所覆盖的区域,第二次是在这个区域的像素里去着色。

也是为了性能优化,Simple Light就是粒子里创建的简单光源,通常它是在Clustered Local Light阶段绘制的,如果没有开启Clustered Local Light,才会有自己的pass,它的过程和Local Light的pass类似。

另外在虚幻5.1中,移动端也支持了延迟渲染的Lighting Channel,不过我们的方式跟PC上的不太一样,因为PC上是把Lighting Channel,写到Stencil Buffer里,然后在shader里面去读取,这个对于移动设备来说有点困难,因为OpenGLES不支持TexureView的扩展,所以我们在硬件上,就是直接在Stencil Test上去做的。

我们尝试了两种方案,一种是我这里列出来的先遍历所有灯光,然后再遍历所有的Lighting Channel,然后通过stencil test对受影响的像素着色。如果有多个Lighting Channel,还需要在stencil里去标记哪些像素已经被着色过,以免重复照亮这些像素,最后再把这个stencil的值清除掉。

这个方案的好处是,一个灯光可以支持多个Lighting Channel,但是就是过程有点复杂,有可能会有一些额外的性能消耗,所以我们选择了第二个方案。

就是我们只遍历所有的灯光,只做一次Lighting Channel的stencil,整个过程会简单很多,但是每一盏灯就只支持一个Lighting Channel,如果有多个Lighting Channel的话就会出错,这也是美术上需要留意的地方。

接下来我们看一下,我们遇到过哪些兼容性问题。

第一个是我们在一些Adreno 660设备和Mali G57的设备上,我们看到延迟渲染的效果不太对,但是在其它设备上是正常的,后来我们发现是因为GBufferC用了sRGB的格式导致的,如果不用sRGB格式就可以修复这个问题。

第二个问题是我们在支持Clustered Reflection的时候,我们发现FReflectionCaptureShaderData Uniform Buffer,它会申请一个长度是2048的静态数组,但是因为我们在OpenGLES上是打开了Emulated uniform buffer的方式,它就会造成编译失败。

因为它相当于是用uniform component去存储,但是因为设备上只支持1024个Uniform Component,所以会造成设备上不支持。我们就想了个办法,支持了一种混合的方式可以单独标记这个结构,使用真的Uniform Buffer来创建,这样就可以正常编译了。

第三个兼容性问题,是我们有一些pass之前没有考虑过,我们会有MRT的形式,所以没有设置这些MRT的BlendState,只设置了SceneColor,其余的MRT默认的都是可写的状态。

但是在Shader里面我们其实并没有对这些RT输出过任何数值,我们发现在一些PowerVR的设备上这些RT的内容被覆盖了被清空了导致我们的渲染出错,所以我们就把这些相关的pass全都设置了MRT的BlendState,改成了不可写的状态,这样子这个问题就解决了。

以上就是我们遇到的一些比较有代表性的兼容性问题,我们再来看一下针对延迟渲染我们有做了哪些优化。

我们主要做了两方面的优化,一方面是带宽的优化,我们尽量尽可能不去占用带宽,只有必要的情况下才去把数据存下来。比如这个DepthAux我们就可以直接成memory less,因为我们不支持MSAA,所以不用考虑是不是有MSAA的问题。

第二个是GBuffers,我们也可以memoryless,这样也不用占用带宽,还有就是SceneDepth,我们只在需要的时候才会用到,如果PP阶段没有用到Depth的信息,我们就可以不把它存下来。

另一方面我们是对性能也做了一点优化,我们发现开启延迟渲染后,半透明阶段的渲染耗时比以前高,后来发现是因为半透明阶段默认也开了Clustered Local Light和Clustered Reflection,这个对性能有比较明显的影响。然后我们本身对于半透明的光照和反射效果的需求并没有那么高,所以我们改了一下,可以在半透明阶段关掉这些计算,这样半透明的效率就和以前持平了。

最后我列了一下虚幻4和虚幻5延迟渲染的一些主要差异,方便现在用虚幻4的团队去做一个对比,然后如果他们有什么需求的话,也好从虚幻5里面去改动过来。

我今天的内容差不多就结束了,谢谢大家!

如若转载,请注明出处:http://www.gamelook.com.cn/2022/08/494436

关注微信