赞
踩
本系列为一些性能优化的小知识,是日常游戏开发中与性能表现的一些点,本篇为该系列文章的第二篇,前篇链接:
第一篇: Unity性能优化:资源篇
在早期Unity
中,对于合批的处理手段主要是下面三种:
Static Batching
Dynamic Batching
GPU Instancing
并且对于他们有着严格的使用限制,而在Unity
推出SPR
后,为了提升合批的范围与效率,提供了新的合批方式SPR Batcher
,本篇文章就简单的介绍一下这些合批的技术
在开始了解合批之前,需要理解一些衡量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
的官方文档,有些是直接复制过来的,信息还是比较准确的:
根据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
,具体位置如下图所示:
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
使用 GPU Instanceing
可使用少量绘制调用一次绘制(或渲染)同一网格的多个副本。它对于绘制诸如建筑物、树木和草地之类的在场景中重复出现的对象非常有用:
GPU Instanceing
在每次绘制调用时仅渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或比例)以增加变化并减少外观上的重复。
GPU Instanceing
可以降低每个场景使用的绘制调用数量。可以显著提高项目的渲染性能。
与其他合批手段类似,GPU Instanceing
同样有一些使用限制条件:
Unity
自动选取要实例化的网格渲染器组件和 Graphics.DrawMesh
调用。请注意,不支持 SkinnedMeshRenderer
Unity
仅在单个GPU
实例化绘制调用中批量处理那些共享相同网格和相同材质的游戏对象。使用少量网格和材质可以提高实例化效率。要创建变体,请修改着色器脚本为每个实例添加数据
关于该描述的官方链接为:GPU实例化
上面是官方文档对于GPU Instanceing
的一些描述,可以看出与其他两种合批手段不同的是,除了材质相同之外,其主要是对于使用同一网格的物体有效,所以正如名字的Instanceing
那样,是通过GPU
直接对于某一物体进行实例化来降低CPU
对场景物体的数据命令准备所产生的性能消耗的技术手段
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
通过批处理一系列 Bind
和Draw 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 buffer
的 Unity
引擎属性。所有材质在 GPU
内存中都有持久的 CBUFFER
,可供随时使用。这样会加快渲染速度,原因是: 现在,所有材质内容都持久保留在 GPU
内存中。 专用代码针对所有每对象属性,管理着一个大型的每对象GPU CBUFFER
SRP Batcher 限制条件:
为了使 SRP Batcher
代码路径能够渲染对象:
渲染的对象必须是网格或蒙皮网格。该对象不能是粒子。
着色器必须与 SRP Batcher
兼容。HDRP
和 URP
中的所有光照和无光照着色器均符合此要求(这些着色器的“粒子”版本除外)。
为了使着色器与 SRP Batcher 兼容:
必须在一个名为UnityPerDraw
的 CBUFFER
中声明所有内置引擎属性。例如unity_ObjectToWorld
或 unity_SHAr
必须在一个名为 UnityPerMaterial
的 CBUFFER
中声明所有材质属性
传统合批:
一般来说,合批是为了减少场景渲染的数据处理,进而降低渲染时的CPU
压力,通过Unity
的性能分析工具Profiler
可以方便的看到有关于与之有关的数值:
点击Rendering
就可以在Open Frame Debugger
面板看到与合批有关的知识,具体到参与Static Batching
、 Dynamic Batching
、 GPU 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
的对象在上面的截图分析的场景中,放置了30000
个Cube
,分别对其进行不同的批处理操作,来分析整个合批的过程中的资源消耗情况,对其进行Draw Call
与用时的统计,由于Dynamic Batching
使用场景苛刻且对于CPU
性能表现不明显,本处剔除呢该合批手段,场景中物体的具体合批方式为:
Static Batching
GPU Instancing
Dynamic Batching
通过Profiler
对CPU
性能表现的监控发现,三种合批手段的运行效率静态合批最高,GPU Instancing
与Dynamic Batching
相对比较差,值得注意的是,当场景中物体比较多时,通过上述分析方式观察和得到的数值显示的GPU Instancing
的耗时是多于Dynamic Batching
的,但是事实上两种合批技术在CPU
的整个渲染阶段总耗时是反过来的,我们将Profile
的Hierarchy
模式切换为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
中的合批手段,成熟有效的有上面几种方式,他们各有优劣,需要根据实际的应用场景来选择合适的方式,简单的来说,如果你的内存预算十分有限,那么就不要考虑静态合批避免增加内存的压力。在得到这些技术好处时,不要忘记了你付出的代价
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。