6

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

 1 year ago
source link: http://www.gamelook.com.cn/2023/05/517055
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2023-05-06 • 游戏美术

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

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

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

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

Xiaoxin Guo:

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

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

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

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

lazy.png

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

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

实时直接照明

lazy.png

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

lazy.png

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

lazy.png

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

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

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

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

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

动态烘焙

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

lazy.png

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

lazy.png

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

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

lazy.png

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

lazy.png

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

lazy.png

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

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

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

lazy.png

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

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

lazy.png

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

lazy.png

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

lazy.png

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

lazy.png

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

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

lazy.png

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

lazy.png

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

lazy.png

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

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

lazy.png

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

lazy.png

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

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

lazy.png

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

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

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

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

lazy.png

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

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

lazy.png

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

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

lazy.png

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

lazy.png

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

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

lazy.png

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

lazy.png

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

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

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

lazy.png

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK