GDC | 腾讯游戏Xiaoxin Guo:移动平台的高性能渲染实用技巧

【GameLook专稿,未经授权不得转载!】

GameLook报道/在硬件兼容性和性能方面,《王者荣耀》的渲染是非常具有挑战性的,目标硬件包括入门级手机到最新的款式。另外,团队还必须在不牺牲核心体验的情况下支持1亿以上的DAU。

腾讯控股高级渲染工程师Xiaoxin Guo详细讲述游戏渲染管线,提及了一些帮助他们将游戏做到1080p 60fps运行、面向各种硬件配置的优化方法。

以下是Gamelook听译的完整内容:

Xiaoxin Guo:

我是来自腾讯游戏天美L1工作室的高级渲染工程师Xiaoxin Guo,我的同事Tianyu Li与我和我们的工程师团队合作来呈现了今天的演讲:“移动平台的实用高性能渲染技巧”。不幸的是,Tianyu无法出席今年的GDC大会,所以她那部分也由我代替。

这次分享中,我们会讨论到在移动平台研发渲染技术时遇到的挑战,以及我们是如何解决的。开始之前,我想先展示一个游戏视频:

这个视频展示了游戏内两个渲染场景,一个是玩游戏时的俯视视角,另一个是电影场景,今天的讨论将聚焦于后者。

作为开始,我会先介绍一下我们的整体架构,随后会讨论一些话题,比如实时阴影、动态烘焙、光泽反射以及预计算可视锥(Pre-computed visibility cone)。

我们在渲染平台使用正向渲染以支持游戏需要的不同材质类型,包括照片级写实和非照片级写实风格。我们的光照系统有各种组件,包括实时精确光源、动态和静态光源,以及基于标准化图像的光源。

实时精准光照方面,我们还支持有限的定向聚光灯和指示灯。此外,我们支持其他光源类型,包括用于动态和静态烘焙的不规则光源(mesh light),然而,由于性能限制,动态烘焙光源的数量是有限的。

实时直接照明

在这部分,我们将讨论实时直接照明(Real-time Direct Lighting)以及与它相关的挑战,可以从图片里看到,阴影是直接照明的必要视觉现象和实时渲染里的微妙问题。阴影贴图(shadow mapping)是最常用的技巧之一,然而这个技巧却并非没有伪影,受到性能限制和平台差异的影响,当我们在移动平台使用的时候会面临更多困难。

在阴影贴图研发的时候,我们遭遇的问题之一是意外出现的阴影边界球体,边界球体实际上是一个覆盖所有物体并投射阴影的球体。通常来说,它通常由PSSM管理,即Parallel-Split Shadow Maps。然而,由于伪影的存在,它并不适合我们的游戏。

对于聚光灯,它使用较大的开敞角(opening angle),导致阴影贴图纹素密度(texel density)较低。由于我们的艺术选择,我们可能要使用一个非常大的开敞角,它是没办法做动画的。

可以通过这张图片看出伪影的结果,这个截屏中,灯光照亮了机器人和地面,并覆盖了一个很大的区域,然而只有机器人投射阴影,所以你应该为之使用更紧密的阴影边界球(bounding sphere)。

这类情形在资源制作的过程中经常发生,我们注意到PSSM是一个管理阴影边界球的方法,我们可以概括这个想法,并将其运用到聚光灯以提升阴影质量。

因此,这种技术的部署是简单的:美术师将边界球放到场景中,我们在阴影恢复(shadow map restoration path)路径循环所有可视光源。对于每个光源,第一步是为当前光源覆盖可视边界球,随后通过纹素密度分类,最后为阴影边界球渲染Shadow Split列表。

为了用边界球渲染Shadow Split,我们需要用不同的方式处理视角和映射,这一点稍后会做出解释。为了在pixel(shader)阶段测试光源可视度,我们为当前光源循环分类的shadow split,然后用第一个有效的,因为它比其他的有着更高的质量。

这个方法是PSSM渲染的概括化,所以直接光源与PSSM对比是相同的,因此我会分享一些细节。

对于聚光灯的shadow split渲染器,我们使用了透视映射,纹素缩放和偏移以将边界球重新映射到阴影贴图的中心。

我们微调了阴影边界球,然后发现阴影质量有了很大的提升。阴影贴图技巧的另一个问题是过采样(oversampling)或欠采样(undersampling)导致的阴影失真(shadow acne),想要完全解决这个问题是很困难的。

为降低伪影,我们在阴影贴图栅格化的时候应用了一个depth slope offset,并且在阴影贴图过滤的时候运用了小的normal offset,然而,这需要逐个光源进行调整,所以没有一个能够适合所有参数的解决方案。

另外,depth slope offset可能会给跨平台研发带来问题,因为对于不同的图形API和depth buffer格式,它们的行为也不同。而且,在PC平台的美术调整参数,放到手机上看起来可能很糟糕。

所以,我们的目标是研发一种能够满足一下要求的depth slope offset技巧:高性能、耐用;支持直接光源、聚光灯和点光源;与图形API以及其他参数独立开来,比如阴影贴图分辨率、阴影纹理格式以及阴影边界球等等。

为了理解depth slope offset,我们首先必须将depth value的最大横向和纵向slope分开,我们可以在pixel shader里用ddx和ddy函数进行这样部署。然而,对于高性能部署,我们无法承担在pixel shader里计算,相反,我们可以在顶点着色器里计算,它利用射线微分得到三角形的解析解。

射线微分在计算机图形学里有很多的应用,其中一个就是深度纹理贴图过滤。射线微分背后的关键想法是用它来计算一些值的偏导数,在我们的案例中是depth slope,使用射线微分在我们的案例中要用到辅助面法线,但我们在顶点着色器里并没有辅助,所以可以通过顶点法线估算面法线。幸运的是,我们通过这个估算的结果没有看到任何伪影。

想要通过射线微分在顶点着色器里计算depth slope offset,我们使用了两个坐标射线与三角形相交。CPU方面,我们计算pixel坐标位置的偏导数,可以看到分别是dp_dx和dp_dy,它在整个屏幕里都是持续的。

阴影贴图在GPU上光栅化期间,我们根据映射类型用不同的方式计算所有的辅助射线。对于正投影,我们用dp_dx和dp_dy抵消射线源,同时保持射线方向不变;对于透视映射,我们将射线源保持变,并计算当前射线与附近面的交叉,用dp_dx和dp_dy抵消附近面交叉点,常态化以获得辅助射线方向。

随后,我们既可以将其与当前三角形交叉,这可以给我们三个交叉点,我们随后在真实空间计算交叉点的深度,并将其用作depth slope以抵消顶点。

通过这个方面,我们在顶点着色器增加了大约40个ALU,尽管这增加了复杂度,但我们并没有发现对性能产生负面影响。

动态烘焙

对于直接照明和静态烘焙,我们开始有了一些看起来不错的方法。然而这有些枯燥,我们的光源有限,而且一切都是静态烘焙的,那么,我们能做到更好吗?

通过动态烘焙,我们当然可以做到更好。

通过这个动图,可以看到动态烘焙能实现令人印象深刻的效果。

那么,动态烘焙到底是什么?简短来说,它是一种提供低延迟、低runtime开销的光照数据管理方法,并且支持动态光照以及通过改变光照状态导致的全局照明(Global illumination),同时,它也支持人为光源,包括不规则光源。

这里是一个动态烘焙的使用案例,随后我们会探讨部署的细节。

最简单的部署是为每个光源烘焙不同的光照贴图,对他们进行采样,在放射的时候进行标记。然而,这种方式导致光照贴图数量与动态光源成比例,这不适合密集的内存使用。

为解决这个问题,我们需要设计一个有效的数据结构,它需要满足低内存使用和低runtime开销,这时候就需要用到Receiver。

我们最初的想法是为每个动态光源存储一个a sparse texel buffer,而且只存储有用的数据。

随后可能的部署可以是一个compute cone,问题在于我们要重写运营,也就是读取当前Receiver的光照贴图纹素,我们计算新值然后写回去。

我们在移动平台有三个方法解决这个问题:第一个是UAV,第二是Ping-Pong纹理,第三是Frambuffer Fetch。很多的移动设备对于UAV和Frambuffer Fetch的支持都不是很好,Ping-Pong纹理看起来够用但需要更多的内存分配。

即便是以上问题都可以被忽略,重写运营本身对于并行计算而言都是很难的,是的这种部署变得不切实际。那么,我们该如何摆脱重写操作?

与为光源存储光照贴图不同的是,我们实际上可以为纹素存储多个光照贴图。有了这个数据结构,最终写入结果是等待的光照贴图样本总和,避免了重写操作的需要。

计算管线的Receiver部署只需要structured buffer,光照贴图使用UAV。在计算阶段,我们只是分析receiver信息,并将其写入目标像素位置。但对于移动设备,我们必须覆盖没有强大计算管线支持的设备,因此对于这些设备,我们使用了图形管线而非计算管线,我们将数据用point topology存储到顶点buffer。

这张图片中,我们可以看到很多点能够对Receiver作出回应。

我们还限制了能够影响物体的光源数量为3个,这给了美术师足够多的光照灵活性,而不用进行重大的性能检测。这种局限可以通过用additive blend模式多次调用来解决,但在我们的资源之作过程中,并没有发现用例。我们还提供工具用来帮助美术师们进行可视化debug。

为了让部署性能在所有层次的硬件上足够用,我们在数帧之间使用不同的更新节奏分发计算,所以它们是checkboard和2×2方块,每个checkboard都代表每帧更新半个纹素,2×2方块则每帧更新四分之一纹素。

我们保留了一个全局光源改变频率,并基于它选择更新节奏,所以视觉上看不出更新变化。

你们可能注意到,我们光照贴图上一些纹素看起来更暗,这是因为辐照度分布(the distribution of irradiance)通常是长尾节奏。我们不希望存储任何不必要的纹素,所以我们通常根据亮度对纹素分类和固定。

然而,固定纹素导致我们的光照数据中断。

为了解决这个问题,我们将亮度阈值设定为更低的边界,因为我们想要的是平滑,保持亮像素不至于模糊,所以我们使用了比较保守的阈值,也就是α边界,可以通过图片看到平滑前后的对比。

对于内存使用效率,我们使用了float3+RGBA32顶点格式,每个纹素与一个顶点对应。对于性能表现,我们要求一个纹理读取和一个纹理存储,同时对光照贴图更新采用每个纹素130 ALU。ALU的数量取决于光照贴图编码选择以及动态光源的数量。

有了直接光源和烘焙光,我们即将完成顶点渲染(pixel shading),最后一部分是增加光泽反射是(glossy reflection)。光泽反射对于场景整体分辨率光照而言是非常重要的元素。

PC平台的反射是通过一个分层解决方案,首先是射线追踪反射(ray traced reflection)或屏幕空间反射(screen space reflection),如果没有交叉,它就返回到局部光探针或平面反射(planar reflection),最终到全局光探针。

但是,对于移动平台,由于资源有限,我们对于室内和室外光照都只能使用单个光探针。为了让它可行,我们将基于图像的灯光正常化,为了更进一步,我们还使用来自动态烘焙的光照变体。

这张动图可以展示以上提到的概念。

我们还通过预先计算的可视锥计算镜面遮挡来缓解光泄露问题,这可以带来更高的图形质量。

这个想法来自于动视《使命召唤》里的预计算光照,锥体数据包括轴、光圈和缩放,可以被编译为4个浮点。这个数据可以存储为网格或纹理顶点属性。

与计算可视锥的计算非常直接,在每个网格表面上的每个样本处,追踪每个方向的光线,评估距离衰减的可视函数,并将其投影到SH上。通过具有最小二乘拟合的锥体估算SH函数,解析求解。

我建议阅读最初的演示以了解更多细节。

可视锥数据可以存储为顶点属性,很多情况下,这是优先选择,因为它的存储更紧凑、格式更灵活,比如FP16、FP32、UNorm等等。另外,也可以避免UV wraping。

由于三角形插值,在顶点着色器计算可视锥可能会导致伪影,最小二乘法是解决该问题的一个方法,部署起来也很简单。在网格表面调用粗糙样本,计算可视信息,然后通过最小二乘法获得pre-vertex数据和gradient-based regularization,来自英伟达的部署参考也是可以阅读的,如果你对这个想法不熟悉,我强烈建议阅读初始论文:https://github.com/nvpro-samples/optix_prime_baking

虽然部署简单,但我们在执行的过程中遭遇了论文中没有提及的一些问题,这个截图可以看到明显的断续伪影。

伪影的出现是因为空间闭合顶点(spatially closed vertex)不能位于相邻三角形上,导致线性系统内的冲突。为解决这个问题,我们使用了一个proxy网格打造线性系统,然后将proxy网格的烘焙结果映射到初始网格。对于proxy 几何体,我们使用了position hashing和normal hashing。

解决了这个问题之后,我们就可以消灭伪影现象。

一旦可视锥数据就绪,我们就可以计算specular和它的环境遮挡,为此,我们可以通过锥体估算BRDF,并评估BSDF锥体和可视锥的交叉。然而,这个方法的性能很差,导致掠角过暗。

它要求60个ALU左右,而且锥体交叉并不为地平线裁剪(horizon clipping)说明。

过暗伪影可以在这张图看到,尤其是地面。我们还发现伪影取决于材质的粗糙度,这让伪影更加明显。

我们对于specular occlusion的最终解决方案就是使用LUT,由于LUT是离线预计算的,我们可以使用BRDF和cone integration。

对我们来说,将specular occlusion烘焙成16x16x16的3D LUT,对我们就是非常可行的。我们可以使用cone aperture、材质粗糙度等作为参数,我们相信未来可以找到数字拟合方法,而不是现在的LUT。

使用LUT的最终效果比之前的方法好很多。

我们可以使用可视锥数据来计算环境遮挡,但它与计算speculator occlusion几乎一样。不过,这次我们使用了扩散BRDF,而且有解析解。

为优化计算,我们首先忽略了cos sin,所以我们的cone aperture只有一个D函数,可以看到,即便是开销最低的方案,差异也可以忽略不计。

然而,这会导致法线贴图协作问题,法线贴图会使预先计算的数据无效,从而更改曲面几何体,解决起来很棘手。

为此,我们采取了一种基于启发式的校正方法。

可以看到这种方法的结果与光线追踪环境遮挡很接近。

性能方面,我们考虑内存占用和性能,对内存占用,我们为每个顶点增加了float4,并使用FP16节约内存。有了3D LUT,我们可以根据目标平台选择不压缩或ASTC压缩数据,不压缩数据只占用4KB。

对于性能,我们对specular occlusion使用一个纹理读取,为diffusion occlusion以FP16使用8个ALU。以上就是今天的分享。

如若转载,请注明出处:http://www.gamelook.com.cn/2023/05/517055

关注微信