大规模渲染:处理海量对象的高效策略

这是 Mega Cat Studios 撰写的系列文章的第四篇。在这篇文章中,Matthew Wojtechko 探讨了使用 URP 和HDRP所获得的性能经验教训,并探索了诸如自定义批量渲染器组实现等高级解决方案,用于草地渲染,因为每一毫秒都至关重要。
阅读本系列的其他博文:
在不掉帧的情况下渲染数千个物体始终是一个挑战。密集的城市景观和广阔的风景已成为现代游戏的标志。虽然这些景色令玩家叹为观止,但对于负责保持每秒 60 帧 (fps) 流畅运行的工程师来说,却可能是一个令人头疼的问题。
如果你瞄准的是性能较弱的平台,例如移动设备或无线设备,那么问题不仅仅在于这些视野。任何环境,只要包含任何程度的细节,都容易导致硬件过载。
在Mega Cat Studios ,我们多年来一直在不断探索 Unity 渲染系统的功能,为了让《后院棒球》这款游戏可爱的艺术风格能够在 PC 和主机上运行,同时又不牺牲艺术意图,我们使出了浑身解数。
在这篇文章中,我们将探讨静态批处理和顶点动画纹理等技术,并分享我们在迄今为止最大的3D游戏中遇到的一些问题,所有这些都是为了帮助您在对象数量爆炸式增长时保持帧权重的流畅性。

调查导致渲染问题的原因,然后进行优化。
渲染是CPU和GPU共同协作完成的。
“CPU是教练,GPU是运动员。”“CPU负责发出指令,GPU负责执行,” 《后院棒球》的优化工程师利亚姆·杜达斯说道。
有时,GPU 需要执行的工作比其他任务更耗时。复杂的网格、着色器和光照会导致速度变慢,提高性能的最佳方法是降低它们的复杂性,要求美术师优化这些素材资源。
有时候,问题不在于 CPU 要求 GPU 做什么,而在于它如何要求 GPU 做。
CPU是否以最优方式执行其指令?是不是因为计算绘制内容耗时过长,导致 CPU 闲着没事干,无法让游戏的其余部分正常运行?CPU是否在提供冗余信息?一位优秀的教练可以激励他的球队上演意想不到的逆转,但就优化而言,我们不能只是让这些系统“更加努力”。关键在于理解他们处理的信息以及他们如何传达这些信息。
归根结底,就是优化 CPU 传递给 GPU 的绘制调用。一次调用中能处理的工作越多越好,尤其是当需要渲染数万个对象时。
有很多方法可以做到这一点——但在你这样做之前,请注意以下事项。
不要为了追求编写巧妙代码的快感而随意进行优化。始终先进行基准测试。找出项目遇到的最大问题所在。你首先应该问的问题是:瓶颈在于CPU还是GPU?
本文中的一些解决方案可以提高 CPU 速度,另一些则可以提高 GPU 速度。你不想因为过度优化一个处理器而忽略了另一个处理器,而实际上另一个处理器才是导致帧率波动的原因。一旦确定你的游戏是受 CPU 限制还是受 GPU 限制,就调查哪个过程耗时最长,然后优化它。Unity 的性能分析最佳实践指南是一个很好的起点,其中包含一个方便的流程图,为性能分析这个通常很神秘的过程提供了指导。
“我需要做很多用户画像分析,”利亚姆说。“我可以分辨出 CPU 是否在大量工作而 GPU 是否在大量工作,反之亦然。实际上,我们遇到的最大瓶颈都是他们俩在不同时期造成的。
我们在本文末尾列出了一系列我们最喜欢的资源,以帮助您进行自己的个人资料完善之旅。
免责声明完毕,接下来让我们探讨优化方案。
简化和精简
渲染时,我们需要确定要渲染哪些网格以及渲染位置。
网格越多,需要的绘制调用越多,CPU 就越需要不断地思考该做什么,而 GPU 则需要等待下一个任务。
我们可以通过减少渲染的网格数量来加快处理速度。例如,你可以用冒牌货、广告牌或低多边形版本替换复杂的网格;或者你可以将一个庞大的世界分成更小的关卡,这样一次需要渲染的内容就更少了。这些都是经过实践检验的有效解决方案,你应该尽可能使用它们,但它们会限制你环境中的细节程度。
避免这种情况的一种优化方法是遮挡剔除,这种方法很容易实现,它可以让摄像机检查视野范围内的内容,并避免渲染玩家看不到的内容。这样可以节省时间,但会增加内存使用量,不过在有很多固定网格的环境中,比如办公楼里的细节丰富的房间,这样做通常是值得的。这种优化是行业标准,但单靠它往往还不够。
静态批处理
另一种在不牺牲多样性的前提下限制渲染网格数量的方法是将它们合并成一个网格。
你可以自己手动完成这个操作。或者,只要网格共享相同的材质, Unity就可以通过称为批处理的过程来为你完成此操作。将游戏对象设置为静态, Unity会自动将所有具有相同材质的网格批量处理,以减少绘制调用次数。此选项适用于任何静止不动的物体,例如树木和墙壁。
这大大降低了内存使用量和CPU开销,使GPU能够一次渲染更多内容。但请记住,存储组合网格将占用更多 GPU 内存。
静态批处理通常是我们最重要的优化流程,特别是对于Backyard Baseball而言。使用 URP 的批量系统很简单,因此最具挑战性的部分是如何在尽可能多的模型中使用尽可能少的材料。正是这种平衡之道,才使得我们的优化团队和美术团队拥有一些最具创造力的人才。
GPU实例化
现在是时候开始区分事物本身……和该事物的一个分层了。
假设你有一个网格。网格是一种资产。而且它的原材料也是一种资产。它们是存在于文件结构中的单个文件。在游戏中,每个具有3D外观的对象都由网格描述其形状,然后由材质描述其表面外观。
但由于Unity简化了细节,一些开发者没有意识到,当他们的游戏运行时,实际上在游戏世界中实例化了这些对象的多个副本。当玩家探索森林时,他们实际上是在穿过游戏文件中存储的同一棵树的许多副本。
这就是为什么我们在《后院棒球》中使用 GPU 实例化来模拟树叶的原因。CPU 不再使用单独的 GPU 调用来渲染树的每个副本,而是要求 GPU 一次性渲染所有相同的模型。如果不小心,这可能会导致森林里的每一棵树看起来都一模一样。但是,通过在材质着色器中添加一些有效的参数(例如,用于自定义树叶颜色的值),您可以将一个模型和材质以及每棵树的独特颜色传递给 GPU,而不是单独请求每棵树的颜色。

在材质上启用“启用实例”设置后, Unity会将使用相同材质的不同材质实例合并到同一个绘制调用中。这是平衡网格多样性和性能的绝佳方法。而且,与静态批处理不同,GPU 实例化可以用于场景中移动的游戏对象,因此您不仅限于优化墙壁和树木之类的东西,还可以优化残骸碎片或办公用品之类的道具。
顶点动画纹理
静态批处理和 GPU 实例化是优化网格和材质的两种不同方法。但有时,造成瓶颈的却是动画。以前面提到的简单森林例子为例。现在,想象一下它的枝叶在微风中摇曳,一切都呈现出独特而自然的动态效果。用常规方式运行 10,000 个独特的动画流程是远远不够的。如果你不小心,可怜的CPU就要窒息而死了!
一个技巧是使用顶点动画纹理(VAT)。VAT 程序会劫持 GPU 的处理能力来尝试进行动画制作。
对于 Mega Cat Studios 的高级开发人员 David Chávez Armenteros 来说,使用 VAT 来模拟人群是他最喜欢优化的图形实现之一。
他说:“使用动画器中的标准蒙皮网格渲染器,只能渲染几十个角色,之后性能就会开始下降。”“但是,通过将 VAT 与 GPU 实例化相结合,您可以一次性在屏幕上推送数千个动画实体。”
使用 VAT 时,每个网格顶点每帧都会从纹理中读取其动画位置。为此,我们将位置和旋转数据编码为RGB值。然后,一个特殊的着色器读取纹理中的所有像素,以确定如何移动所有顶点。
这就是我们在《后院棒球》游戏中用来制作摇曳草地效果的素材。这是创建流畅动画的有效方法,但请记住,它只是视觉上的——碰撞器不会改变。这使得VAT非常适合那些需要移动但不需要精确碰撞检测的场景。

每项优化都伴随着权衡取舍。增值税可以释放 CPU 资源,但会增加内存的负担。所有这些都需要与游戏中其他渲染方案进行权衡。
“该系统最棘手的地方在于平衡内存使用和基于 GPU 计算的剔除计算,以应对无线设备的有限资源,” 《后院棒球》的技术美术师 Jordan Latta 说。
URP 与HDRP
优化图形时,首先要做的选择是在开始一个项目时选择哪种渲染管线。通用渲染管线 (URP) 具有良好的基础性能,是移动设备和无线设备的绝对必备之选。高清渲染管线 (HDRP) 旨在实现高保真视觉效果,并提供更专业的实例化行为。
通常,我们建议在Unity中使用 URP 而不是HDRP 。或许您有需要使用HDRP的用例,但我们的出发点是一个简单的原则:尽可能提高渲染效率。
HDRP提供了一些高端功能,但如果您知道自己并不追求逼真的视觉效果,我们建议您不要选择它。
更多优化技巧
有时候,屏幕上出现成千上万个物体的问题根本不在于渲染。这是你自己的代码。在这种情况下,您可以采用以下一些技巧:
- 将数千个 Update 循环合并为一个。不要给每个 GameObject 都赋予自己的 MonoBehaviour,而是将它们合并成一个,使它们共享同一个 Update 函数。集中式管理器有助于避免数千个单独的更新循环。
- 不要在同一个线程中运行所有程序,而应该使用多线程。探索一下工作系统和Burst编译器是否适合你的游戏。
- 利用藏匿点赚钱。不要每帧都获取主摄像机或游戏对象的变换,而是缓存一次,然后重复使用该引用。Tarodev展示了性能提升,并解释了其他优化措施。
或许,提升下一个项目渲染效果最显著的方法就是使用Unity的最新 LTS 版本。Unity 6.3 LTS拥有诸多改进,包括提升性能的优化。
对于《后院棒球》这款游戏,我们最简单、最好的优化成果就是控制纹理的使用。
有些不易察觉的方法会导致你不小心使用了双倍的纹理:
- 着色器:如果着色器在两个不同的场合使用同一个纹理,并且没有共享同一个引用,则可能会在内存中的多个位置加载该纹理。
- 脚本:同样,代码库的不同部分可能会通过不同的引用加载相同的纹理。
- Version Control:当两个开发者将同一个文件添加到他们的本地分支时,有时在分支合并时两个文件都会被保留,从而占用了冗余的存储空间。
我们努力意识到这些陷阱,以便在它们发生之前就加以预防,但过于严格会减慢快速的迭代周期。当项目进入开发周期的后期阶段时,我们的优化团队就会介入,审核着色器、素材资源和脚本。
在Mega Cat Studios ,这些策略使我们能够渲染几年前还无法想象的场景。
对我们来说,渲染基本上就像魔法一样:我们可以实时创造出我们能想象到的任何东西,但前提是必须仔细研究那些秘密文本(Unity文档、博客文章、YouTube 教程)。幸运的是,我们团队里有一些技术高手,他们最喜欢的就是深入研究这些文档,并将研究成果与团队分享。
当然,真正受益的还是我们的球员。我们希望粉丝们能够尽情享受探索后院棒球的乐趣,而无需停下来思考为了让这个世界感觉真实,背后付出了多少心血、汗水和魔法。
