Unity性能优化 :合批篇

前言

本系列为一些性能优化的小知识,是日常游戏开发中与性能表现的一些点,本篇为该系列文章的第二篇,前篇链接:

第一篇: Unity性能优化:资源篇


在早期Unity中,对于合批的处理手段主要是下面三种:

  • Static Batching
  • Dynamic Batching
  • GPU Instancing

并且对于他们有着严格的使用限制,而在Unity推出SPR后,为了提升合批的范围与效率,提供了新的合批方式SPR Batcher,本篇文章就简单的介绍一下这些合批的技术

Draw Call、Batcher、 Sat pass Call

在开始了解合批之前,需要理解一些衡量CPU处理渲染速率的参考值

  • Draw Call

Unity引擎前期,衡量CPU在渲染时的资源消耗大多都是是通过Draw Call的数量
因为CPU在渲染流水线中的处理阶段是应用程序阶段,主要是做一些数据的准备与提交工作,而Draw Call的数量代表了CPU向GPU提交的数据的次数,Draw Call本身只是一些数据流的字节,主要的性能消耗在于CPU的数据准备阶段

  • Batcher

由于合批的出现,并不会每一个渲染对象都会产生一个Draw Call,所以这个时候就提出了一个新的衡量标准:Batcher

  • Set Pass Call

前面也说过,CPU在渲染阶段,性能消耗的峰值一般不在于Draw Call,而往往存在于对其数据准备的阶段,因此单纯以数据的提交数量为衡量标准并不准确,同时在数据准备的过程中,假如前后两个材质发生了变化,会更大幅度的消耗性能,这也是整个CPU在渲染阶段最消耗性能的步骤,因此Unity通过Set Pass Call来作为性能消耗的标准

主要的合批技术介绍

下面简单的说明一下在Unity中常用的合批手段,而这些说明主要来源于Unity的官方文档,有些是直接复制过来的,信息还是比较准确的:

1、Static Batching

根据Unity官方文档的表述,Static Batching的工作原理大概如下:

  • 将静态游戏对象转换到世界空间并为它们构建一个共享的顶点和索引缓冲区。
  • 如果已启用 Optimized Mesh Data,则 Unity 会在构建顶点缓冲区时删除任何着色器变体未使用的任何顶点元素。为了执行此操作,系统会进行一些特殊的关键字检查;例如,如果Unity 未检测到 LIGHTMAP_ON关键字,则会从批处理中删除光照贴图 UV
  • 针对同一批次中的可见游戏对象,Unity 会执行一系列简单的绘制调用,每次调用之间几乎没有状态变化。在技术上,Unity不会减少 API绘制调用,而是减少它们之间的状态变化(这正是消耗大量资源的部分)。在大多数平台上,批处理限制为 64k 个顶点和 64k 个索引(OpenGLES 上为 48k 个索引,在 macOS 上为 32k 个索引)

简单的来说,Static Batching通过对一些小的网格进行合并备份到内存中,当执行渲染操作时,CPU一次性将合并的内存的发送给GPU来减少Draw Call的数量,不过这样做有一定的限制:

  • 对象必须是静态的,不可移动
  • 合并的对象使用相同的材质

同时在使用Static Batching时需要额外的内存来存储组合的几何体,导致内存在一定程度上的浪费。简单来说,作为通过内存的上的置换可以获得时间上的高效运行,需要根据实际情况来谨慎添加渲染对象,避免获取CPU性能优势时产生不必要的内存问题

而关于Static Batching的使用,首先需要在Project Setting中的Player选项中勾选Static Batching
在这里插入图片描述

接下来就可以在Inspector面板中对需要Static Batching的对象勾选上Batching Static,具体位置如下图所示:

在这里插入图片描述

2、Dynamic Batching

Dynamic Batching同样是可以对于有共同材质的对象进行相关的合并,但是其对象可以为动态的,而且这一过程是动态进行的,只需要在Project Setting中的Player中勾选上Dynamic Batching即可,但是注意,在URP模板中,这一选项移到了URP的配置文件中,具体位置如图:
在这里插入图片描述

虽然Dynamic Batching的设置步骤很简单,但是其使用条件却很苛刻,需要满足一系列的限定条件,才能实现合批的效果,Unity官方在文档中也做出了详细的罗列:

  • 批处理动态游戏对象在每个顶点都有一定开销,因此批处理仅会应用于总共包含不超过 900 个顶点属性且不超过 300 个顶点的网格。如果着色器使用顶点位置、法线和单个 UV,最多可以批处理 300 个顶点,而如果着色器使用顶点位置、法线、UV0、UV1 和切线,则只能批处理 180 个顶点。

  • 如果游戏对象在变换中包含镜像,则不会对这些对象进行批处理(例如,具有 +1 缩放的游戏对象 A 和具有 –1 缩放的游戏对象 B 无法一起接受批处理)。即使游戏对象基本相同,使用不同的材质实例也会导致游戏对象不能一起接受批处理。例外情况是阴影投射物渲染。

  • 带有光照贴图的游戏对象具有其他渲染器参数:光照贴图索引和光照贴图偏移/缩放。通常,动态光照贴图的游戏对象应指向要批处理的完全相同的光照贴图位置。
    多 pass 着色器会中断批处理。

  • 几乎所有的 Unity 着色器都支持前向渲染中的多个光照,有效地为它们执行额外 pass。“其他每像素光照”的绘制调用不进行批处理。
    旧版延迟(光照 pre-pass)渲染路径会禁用动态批处理,因为它必须绘制两次游戏对象

看起来很多,但是简单的总结大概是模型要简单,同时使用的Shader一定要是单Pass的。同时因为单Pass的限定,对于延迟渲染来说,由于将光照分离到单独的Pass去处理而导致受光的对象完全没有办法进行动态合批的操作,所以会直接屏蔽掉Dynamic Batching

3、GPU Instanceing

使用 GPU Instanceing可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用:

  • GPU Instanceing在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少外观上的重复。

  • GPU Instanceing可以降低每个场景使用的绘制调用数量。可以显著提高项目的渲染性能。

与其他合批手段类似,GPU Instanceing同样有一些使用限制条件:

  • Unity 自动选取要实例化的网格渲染器组件和 Graphics.DrawMesh调用。请注意,不支持 SkinnedMeshRenderer

  • Unity 仅在单个GPU实例化绘制调用中批量处理那些共享相同网格和相同材质的游戏对象。使用少量网格和材质可以提高实例化效率。要创建变体,请修改着色器脚本为每个实例添加数据

关于该描述的官方链接为:GPU实例化

上面是官方文档对于GPU Instanceing的一些描述,可以看出与其他两种合批手段不同的是,除了材质相同之外,其主要是对于使用同一网格的物体有效,所以正如名字的Instanceing那样,是通过GPU直接对于某一物体进行实例化来降低CPU对场景物体的数据命令准备所产生的性能消耗的技术手段

4、SRP Batcher

SRP Batcher的官方文档链接:SRP Batcher,不想去官方文档看也没有关系,我这里也直接做了搬运并稍微加了一些解释性的文字

开启SRP Batch:
要使用 SRP Batcher,项目必须使用可编程渲染管线。可编程渲染管线可以是:

  • 通用渲染管线 (URP)
  • 高清渲染管线 (HDRP)
  • 自定义 SRP

由于后两种方式不常用,所以本文章会基于URP模板来介绍,而关于URP的具体细节,可以查看该文章:Unity 升级项目到Urp(通用渲染管线)以及画面后处理

当我们在项目中使用URP模板后,就可以在资源目录中找到当前项目的URP配置文件,在其中可以看到SRP Batcher的控制选项:
在这里插入图片描述

同时当项目在URP模板下时,Dynamic Batching的开关控制选项也被迁移到了配置文件,但是相比于默认渲染管线该技术默认是被关闭的,因为其相对于SRP Batcher来说并没有优势

SRP Batcher原理:

Unity中,可以在一帧内的任何时间修改任何材质的属性。但是,这种做法有一些缺点。例如,DrawCall 使用新材质时,要执行许多作业。因此,场景中的材质越多,Unity 必须用于设置GPU 数据的 CPU也越多。解决此问题的传统方法是减少 DrawCall的数量以优化CPU 渲染成本,因为 Unity 在发出 DrawCall之前必须进行很多设置。实际的 CPU 成本便来自该设置,而不是来自 GPU DrawCall本身(DrawCall 只是 Unity需要推送到 GPU 命令缓冲区的少量字节)

正如Set Pass Call的描述那样,游戏在渲染阶段CPU的性能消耗主要在与材质切换阶段的一些作业,而SPR Batcher通过在GPU的数据缓冲区的持久化存储来换取CPU的新材质的准备时间,从而降低CPU的数据准备压力

SRP Batcher 通过批处理一系列 BindDraw GPU 命令来减少 DrawCall之间的 GPU 设置,具体过程如图所示:
在这里插入图片描述

为了获得最大渲染性能,这些批次必须尽可能大。为了实现这一点,可以使用尽可能多具有相同着色器的不同材质,但是必须使用尽可能少的着色器变体

在内渲染循环中,当 Unity 检测到新材质时,CPU 会收集所有属性并在 GPU 内存中设置不同的常量缓冲区。GPU缓冲区的数量取决于着色器如何声明其 CBUFFER

为了在场景使用很多不同材质但很少使用着色器变体的一般情况下加快速度,SRP 在原生集成了范例(例如GPU 数据持久性)

SRP Batcher是一个低级渲染循环,使材质数据持久保留在 GPU 内存中。如果材质内容不变,SRP Batcher 不需要设置缓冲区并将缓冲区上传到 GPU。实际上,SRP Batcher 会使用专用的代码路径来快速更新大型 GPU 缓冲区中的 Unity 引擎属性,如下所示:
在这里插入图片描述

这是 SRP Batcher 渲染工作流程。SRP Batcher 使用专用的代码路径来快速更新大型 GPU 缓冲区中的 Unity 引擎属性。在此处,CPU仅处理上图中标记为 Per Object large bufferUnity 引擎属性。所有材质在 GPU 内存中都有持久的 CBUFFER,可供随时使用。这样会加快渲染速度,原因是: 现在,所有材质内容都持久保留在 GPU 内存中。 专用代码针对所有每对象属性,管理着一个大型的每对象GPU CBUFFER

SRP Batcher 限制条件:

为了使 SRP Batcher代码路径能够渲染对象:

  • 渲染的对象必须是网格或蒙皮网格。该对象不能是粒子。

  • 着色器必须与 SRP Batcher 兼容。HDRPURP 中的所有光照和无光照着色器均符合此要求(这些着色器的“粒子”版本除外)。
    为了使着色器与 SRP Batcher 兼容:

  • 必须在一个名为UnityPerDrawCBUFFER 中声明所有内置引擎属性。例如unity_ObjectToWorldunity_SHAr

  • 必须在一个名为 UnityPerMaterialCBUFFER 中声明所有材质属性

对于合批的性能检测

传统合批:

一般来说,合批是为了减少场景渲染的数据处理,进而降低渲染时的CPU压力,通过Unity的性能分析工具Profiler可以方便的看到有关于与之有关的数值:

在这里插入图片描述

点击Rendering就可以在Open Frame Debugger面板看到与合批有关的知识,具体到参与Static BatchingDynamic BatchingGPU Instancing三种合批技术的Draw Call等参数的数量

当然我们也可以对于CPU的性能消耗进行分析,来获取CPU段的瓶颈信息:
在这里插入图片描述

通过点击CPU Usage,可以在下面的面板中看到BatchRendener.Flush,这是一项非常值得关注的可以影响CPU渲染性能的参数,我们可以通过Self的耗时来评估其当前对CPU的影响:

展开后最多可以看到四个子选项:

  • Render.Mesh :对应CPU处理的不能合批的对象
  • Batch.DrawInstanced:对应CPU处理的对于GPU Instancing处理对象
  • Batch.DrawStatic:对应CPU处理的Static Batching的对象
  • Batch.DrawDynamic:对应CPU处理的Dynamic Batching的对象

在上面的截图分析的场景中,放置了30000Cube,分别对其进行不同的批处理操作,来分析整个合批的过程中的资源消耗情况,对其进行Draw Call与用时的统计,由于Dynamic Batching使用场景苛刻且对于CPU性能表现不明显,本处剔除呢该合批手段,场景中物体的具体合批方式为:

  • 10000个物体:Static Batching
  • 10000个物体:GPU Instancing
  • 10000个物体:Dynamic Batching
  • 两个额外的物体:不处理

通过ProfilerCPU性能表现的监控发现,三种合批手段的运行效率静态合批最高,GPU InstancingDynamic Batching相对比较差,值得注意的是,当场景中物体比较多时,通过上述分析方式观察和得到的数值显示的GPU Instancing的耗时是多于Dynamic Batching的,但是事实上两种合批技术在CPU的整个渲染阶段总耗时是反过来的,我们将ProfileHierarchy模式切换为TimeLine,就可以很清晰的看到结果:

在这里插入图片描述

通过上图可以看出,虽然Dynamic Batching在自身产生的耗时(下面的几段短的之和)比较少,但是会造成其对应的BatchRendener.Flush(上面的一段)的耗时增加,所以在分析他们的使用优势时,可以切换为TimeLine模式来分析整体的耗时情况

而关于BatchRendener.Flush的具体内容,可以通过Unity官方论坛内的一名技术人员的描述来理解,这里贴出该人员的原话:
在这里插入图片描述

SRP Batcher:

当我们在项目中开启SRP Batcher后,就会发现其他合批方式不再起作用,就像Static Batching会屏蔽GPU Instancing那样,但是与其不同的是,这块并没有找到具体文档描述,只是一个简单的假设。不过可以简单的做一个实验来印证该说法,在未开启SRP Batcher时,对于几个特定的物体使用动态合批,然后可以在Profiler里面看到成功的实现了静态合批:
在这里插入图片描述
然后打开SRP Batcher开关后:
在这里插入图片描述

所以我这里只能简单的理解为,SRP Batcher会屏蔽其他合批方式,而如果要观察SRP Batcher的性能消耗,可以直接通过TimeLine里面找到SRP Batcher.Flush即可,具体如图:
在这里插入图片描述

总结

关于Unity中的合批手段,成熟有效的有上面几种方式,他们各有优劣,需要根据实际的应用场景来选择合适的方式,简单的来说,如果你的内存预算十分有限,那么就不要考虑静态合批避免增加内存的压力。在得到这些技术好处时,不要忘记了你付出的代价

猜你喜欢

转载自blog.csdn.net/xinzhilinger/article/details/121121772