创建时间
Apr 8, 2023 06:10 PM
标签
Tags
Unity
渲染
性能优化
技术详解
Origin
zhuanlan.zhihu.com
我们曾在四年前对于 Unity 的主流模块的性能优化知识点逐一做过讲解,俗称 “小白版”。随着这几年引擎本身、硬件设备、制作标准等等的升级,UWA 也不断更新优化规则和方法并持续输出给广大开发者。作为 “升级版” 的性能优化手册,【Unity 性能优化系列】将力图以浅显易懂的表达,让更多开发者可以受用。本期就将分享渲染模块相关的知识点。
移动端的优化,渲染是一个逃不掉的话题。作为性能开销的大头,几乎所有的游戏都离不开场景、物体和特效的渲染。如何在优秀的场景视觉效果和流畅的运行中达到最佳的平衡,一直是策划、美术与程序大佬们都头疼的问题。
notion image

一、影响渲染效率的两个最基本的参数:DrawCall 和 Triangle

1、DrawCall 在 GOT Online 的 Overview 模式中,我们可以在渲染模块中看到 DrawCall 曲线,在这个曲线中可以看到具体的的 DrawCall 数量以及 Batch 数量。如下图所示:
notion image
目前,我们建议在中低端机型上 Batch 的主体范围(5%~95%)控制在 [0,250] 以内。
在 Unity 中,我们需要区分 DrawCall 和 Batch。在一个 Batch 中会存在有多个 DrawCall,如下图中 FrameDebugger 中可以看到两个默认的 ParticleSystem 合批成了一个 Batch,这样的一个 Dynamic Batch 中就有 2 个 DrawCall。
notion image
降低 Batch 的方式通常有动态合批、静态合批、GPU Instancing 和 SRP Batcher 这四种,在 UWA Day 2020 中我们分享了 DrawCall 与 Batch 的关系以及这 4 种 Batching 的使用详解,供大家参考:《Unity 移动游戏项目优化案例分析(上)》
2、Triangle 通常情况下,Triangle 面片数越高会导致渲染的耗时越高,因此在我们的报告中提供了 Triangle 的使用情况,并有半透明和不透明的区分。一般建议通过 LOD 工具减少场景中的面片数,进而降低渲染的开销。
notion image
需要说明的是,此处的面片数量并不是当前帧场景模型的面片数,而是当前帧所渲染的面片数,其数值不仅与模型面片数有关,也和渲染次数相关。例如:场景中的网格模型面片数为 1 万,而其使用的 Shader 拥有 2 个渲染 Pass,或者有 2 个相机对其同时渲染,那么此处所显示的 Triangle 数值将为 2 万。

二、Camera.Render 函数堆栈分析

在渲染模块优化中,很有效的方法是通过 Camera.Render 函数的具体堆栈来定位具体的性能瓶颈。这些函数可以在无论是真人真机还是 GOT Online 报告,都可以在【代码效率】中查看。下面是我们优化时常见的几个函数:
1、RenderForward.RenderLoopJob
notion image
在 Camera.Render 展开堆栈中,可以看到 RenderForward.RenderLoopJob 的自身消耗是比较高的,通常是由于 Batch 数量较高导致的。
2、Culling 耗时较高
notion image
一般来说,Culling 的耗时在 10%~20% 的范围是比较合理的。一般 Culling 耗时较高的话,可以通过以下几个方面排查:
1)Culling 耗时与场景中的 GameObject 小物件数量的相关性比较大。这种情况建议研发团队优化场景制作方式 ,关注场景中是否存在过多小物件,导致 Culling 耗时增高。可以考虑采用动态加载、分块显示,或者 Culling Group、Culling Distance 等方法优化 Culling 的耗时。
2)如果项目使用了多线程渲染且开启了 Occlusion Culling,通常会导致子线程的压力过大导致整体 Culling 过高。
由于 Occlusion Culling 需要根据场景中的物体计算遮挡关系,因此开启 Occlusion Culling 虽然降低了渲染消耗,其本身的性能开销却也是值得注意的,并不一定适用于所有场景。这种情况建议研发团队选择性地关闭一部分 Occlusion Culling 去测试一下渲染数据的整体消耗进行对比,再决定是否需要开启这个功能。
3、Render.Mesh Render.Mesh 对应的是无法合批的渲染耗时,它的调用次数对应的是相应的 Batch 数量。下图中,我们可以看到 Render.Mesh 的调用次数为 269,说明场景中有 269 个不透明对象没有进行合批,数量较高。
notion image
Render.Mesh 开销过高,通常是由于不能合批的对象较多导致的,可以从如下几点进行优化:
1)对于不透明的渲染队列,建议对 Material 的冗余进行排查,如原本一样的材质球因为实例不同而导致不能合批,可以通过 UWA 的在线 AssetBundle 检测,对 AssetBundle 中的 Material 冗余进行排查。
notion image
2)对于半透明的渲染队列,需要区分非 NGUI 与 NGUI 的情况,对于使用 NGUI 的情况,Render.Mesh 的调用有很大概率是由 UI 的 DrawCall 导致的,Render.Mesh 调用次数高说明 UI 的 DrawCall 很可能是偏高的,需要排查是否是图集没有合理的打包导致的。
对于非 NGUI 的情况,那需要考虑半透明的对象是否存在穿插的现象,可以通过调整 RenderQueue 来增大相同 Material 的对象进行合批。
4、ParticleSystem.ScheduleGeometryJobs 与 ParticleSystem.Draw
1)ParticleSystem.ScheduleGeometryJobs,是指在 Culling 之前主线程要等待子线程计算 Particle 的位置,然后才能 Culling。往往在战斗界面开销较高。
notion image
对于该函数的优化,建议研发团队考虑在中低端设备上尽可能降低粒子系统的复杂程度,同时尝试通过视域体对其进行预先裁剪,将视域体外部的粒子系统进行 Deactive,从而降低不必要的粒子系统 Schedule 开销。
2)ParticleSystem.Draw 的调用次数对应的是粒子系统的 DrawCall 数量。
notion image
如果该函数调用次数过高,建议研发团队考虑减少粒子系统的数量,可参考 UWA 真人真机测试报告【内存管理 - 具体资源信息 - 粒子系统】中的列表进一步分析和优化。
notion image
另外,可以通过使用 TextureSheetAnimation 的方式,或者通过修改 Order in Layer 减少粒子渲染的穿插从而增大合批的概率,以此来降低 DrawCall。
5、Shader.CreateGPUProgram 该 API 的 CPU 占用是 Shader 第一次渲染时产生的耗时,其耗时与渲染 Shader 的复杂程度相关。
从下图中我们可以看到,在某一帧中 Shader.CreateGPUProgram 的耗时达到了 203.87ms,这个耗时导致游戏的卡顿。
notion image
对此,我们可以将 Shader 通过 ShaderVariantCollection 进行预加载,在加载后通过 ShaderVariantCollection.WarmUp 来触发 Shader.CreateGPUProgram,并将此 SVC 进行缓存,从而避免在游戏运行时触发此 API 的调用,从而避免局部的 CPU 高耗时。

三、开启多线程渲染

开启多线程渲染后,主线程的渲染耗时就会有很明显下降,建议研发团队开启。
notion image
但需要注意的是,由于我们的线上报告的 CPU 时间占用只统计了主线程的耗时,如果版本开启了多线程渲染,在报告中只能看到主线程的耗时,不利于分析渲染瓶颈。因此我们平时建议大家内部测试的时候,提交两个版本,一个开启多线程渲染,作为 Release 版本的渲染耗时参考,一个关闭多线程渲染,用于详细分析渲染瓶颈。

四、GPU Instancing

使用 GPU Instancing 可以一次渲染相同网格的多个副本,但是每个实例可以有不同的参数(例如:Color 或 Scale),以增加变化。在渲染诸如建筑、树木、草等在场景中重复出现的事物时,GPU Instancing 可以有效减少每个场景 DrawCall 数量,显著提升渲染性能。
但是使用 GPU Instancing 有如下注意点:
  • 兼容的平台及 API
  • 渲染实例的网格与材质相同
  • Shader 支持 GPU Instancing
  • 不支持 SkinnedMeshRenderer
在一些特殊情况下,大量半透明物体的 GPU Instancing 渲染耗时可能会带来很高的耗时,这点我们在 UWA DAY 2019 的课程《Unity 引擎渲染、UI、逻辑代码模块的量化分析和优化方法》中做了详细解释。

五、SRP Batcher

越来越多的团队开始使用 URP 作为渲染管线,从而通过 SRP Batcher 大幅提升 Batch 的合批范围,提升渲染效率。使用 URP 时,渲染函数堆栈会变为:
notion image
而在使用 SRP Batcher 时,仍需要注意:
  • Shader 需要兼容 SRP
  • SRP Batcher 暂时不支持粒子系统
  • Shader 变体会打断 DrawCall 的合批
以上就是渲染模块在优化时需要关注的一些问题,如何操作还需要大家结合项目实际情况,同时结合 UWA 服务可以快速地帮助大家定位到性能瓶颈。 > 本文由简悦 SimpRead 转码