《Unity游戏优化》第三章 批处理

批处理原理

批处理:在3D图形中,批处理是一个非常通用的术语,描述了将大量任意数据块组合在一起并将它们作为单个大数据块进行处理的过程。本文中的批处理通常指用于批处理网格数据的机制。
动态批处理和静态批处理这两种方法本质上是几何体合并的两种不同形式,用于将多个对象的网格数据合并到一起,并在单一指令中渲染它们,而不是单独准备和绘制每个几何体。

很多地方模糊的说 批处理原理是是减少Draw Call,其实严格来说并不对。
其中静态批处理并没有减少DC数量。那静态批处理是优化的什么呢?
首先DC是什么就不赘述了,DC之所以造成性能瓶颈是因为CPU每次为提交GPU所准备的数据和设置渲染状态而消耗了太多的时间。所以如果渲染状态不变,在多次DC调用之间并没有渲染状态的切换,渲染API(Command Buffer)会缓存绘制命令,起到了渲染优化的目的 。

批处理提升此过程的性能诀窍在于,新的DC并不一定意味着必须配置新的渲染状态。如果两个对象共享完全相同的渲染状态信息,那么GPU可以立刻开始渲染新对象,因为在最后一个对象完成渲染后,还维护这相同的渲染状态,这消除了由于同步渲染状态而浪费的时间,也减少了需要推入命令缓冲区CommandBuffer中的指令数,减少了CPU和GPU的工作负载。

动态批处理的工作原理:将所有游戏对象顶点转换到 CPU 上的世界空间,所以仅在该工作小于进行绘制调用的情况下,才有优势。绘制调用的资源需求取决于许多因素,主要是使用的图形 API。例如,对于游戏主机或诸如 Apple Metal 之类的现代 API,绘制调用的开销通常低得多,通常动态批处理根本没有优势。------引自2018.4官方文档

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

手动组合彼此接近的游戏对象,比批处理效果更好。例如,一个带有大量抽屉的静态橱柜通常只需在 3D 建模应用程序中或者使用 Mesh.CombineMeshes 来组合成一个网格。----引自2018.4官方文档
https://docs.unity3d.com/2018.4/Documentation/Manual/DrawCallBatching.html
在这里插入图片描述
注意:批次不是越少越好,过大的渲染数据会给内存带宽带来压力,拉高峰值波动上限。

批处理设置

Edit->Project Settings->Player->Other Settings-> disable Static Batching and Dynamic Batching
如果没有开启,可以看看上面设置是否禁用了批处理。
如果禁用了动态批处理,那么多个共用材质的物体并没有减少dc数量,这是因为渲染状态变更的数量并没有真正减少,也没有合并网格信息。渲染管线不知道我们在重复的写入相同的渲染状态,一次一次渲染相同的网格。

Frame Debugger

在这里插入图片描述
这个工具直观的区分两个连续的Draw Call。很容易准确地指出给定的Draw Call渲染了哪些对象。这可以通过查看在Draw Call期间出现了多少个对象,来帮助确定是否对一组对象进行批处理。

单击Frame Debugger中的一个Draw Call项,就会显示标签为“Why this draw call can’t be batched with the previous one(这个Draw Call为什么不能与前一个Draw Call批处理)”的部分。大多数情况下,下方的解释文本说明了哪个条件没有满足(至少是它检测到的首个条件),以及有什么调试批处理行为的有用方法。

1. 动态批处理

动态批处理通过将所有物体的顶点转换为CPU上的世界空间来工作,所以它只能在渲染Draw Call的工作量小于CPU顶点转换工作量的时候,才会起到提高性能的作用。

动态批处理使用场景

  1. 渲染大量的简单网格时,使用大量外观几乎相同的简单物体时。比如:到处是石头、树木和灌木的森林。
  2. 有很多简单而常见的元素(计算机、走廊、管道等)的建筑、工厂等。
  3. 包含很多动态的非动画对象,还包含简单的几何体和粒子特效。

动态批处理优势:

  1. 批处理在运行时动态生成。
  2. 批处理中包含的对象在不同的帧之间可能有所不同,这取决于哪些网格在主摄像机视图中当前是可见的(批处理的内容是动态的)。
  3. 可以在场景中移动的对象也可以进行批处理(对动态对象有效)

动态批处理缺点:

  1. 数量过大时扛不住。
  2. 条件要求限制多(9条要求要求)

动态批处理要求:

  1. 所有网格实例必须使用相同的材质引用。
  2. 只有ParticleSystem和MeshRenderer组件进行动态批处理。SkinnedMeshRenderer组件和所有其他可渲染的组件类型不能进行批处理。
  3. 每个网格至多有300个顶点。
  4. 着色器使用的顶点属性数不能大于900。
  5. 所有网格实例要么使用等比缩放,要么使用非等比缩放,但不能两者混用。
  6. 网格实例应该引用相同的光照纹理文件。
  7. 材质的着色器不能是多Pass的。
  8. 网格实例不能接受实时投影。
  9. 整个批处理中网格索引的总数有上限,这与所用的Graphics API和平台有关,一般索引值在32~64K之间。

解释:
一. 顶点属性包含:定点位置,法线向量,UV坐标,定点颜色等。属性数据越多,900个预算消耗的越多,从而减少了网格允许拥有的定点数量。因此只有相对简单的对象才适合动态批处理。如下图,查看verts值:(只有在引擎内看到的数据为最终数量,在从建模工具中导入时会改变)
在这里插入图片描述
例1:cube 仅仅包含8个顶点,每个顶点有 位置、法线、UV数据,总共24个。远低于300个顶点和900个顶点上限。 可以批处理。
例2:球体包含515个顶点,总共1545个顶点属性,所以不能动态批处理。
二. 网格缩放:
等比缩放:(1,1,1) 和(2,2,2)可以合并,因为是等比缩放,虽然比例不同。
非等不缩放:(2,1,1)(2,2,1)可以合并为另一个,因为是非等比缩放。
注意:负数缩放会导致奇怪情况,这与三个值中哪个是负数无关,与负数值是否为奇数或者偶数有关。同时负数时,渲染顺序还会导致合批失败

动态批处理总结:

两个对象使用不同的纹理,最好合并纹理(通常称为图集),并重新生成网格UV,以便进行动态批处理。这虽然会牺牲纹理的质量或者文件变大,或者纹理文件会变大GPU内存带宽压力大,但这是值得的。

2. 静态批处理

unity静态批处理不是真正意义上的"静态批处理"把勾上的合并成一个mesh,而是首先unity会先按照渲染顺序动态排序以及静态批处理的可视处理。因为球没有勾选static,然后插在了4个cube中间的渲染顺序(或者说挡住了某一个cube时,处于不可视的时候),这样unity就会先渲染2次合并后的Combined Mesh,第一次渲染球前面的Combined Mesh,第二次渲染球后面的combined mesh,因为这个球插足了破坏了合批,所以就是3个batches。而更换角度后,球没有插足cube的渲染顺序,所以就不会破坏合批。(背景,不透明。渲染顺序基本稳定的,先渲染的。)

静态批处理的缺点:

  1. 只能在运行时才能看到效果
  2. 在运行时添加时无效,即使标记为Static对象。
  3. 项目后期启用会花费大量的时间启动 调整 重启 场景,以确保节省了期待节省的DC。最好在早期进行静态批处理的优化。

静态批处理的要求:

  1. 网格必须标记为“静态”。
  2. 每个被静态批处理的网格都需要额外的内存。
  3. 合并到静态批处理中的顶点数量是有上限的,并随着Graphics API和平台的不同而不同,一般为32~64K个顶点。
  4. 网格实例可以来自任何网格数据源,但是它们必须使用相同的材质引用。

静态批处理的内存需求

静态批处理将所有标记为Static的可见网格数据复制到一个更大的网格数据缓冲中,并通过一个Draw Call传到管线渲染中,同时忽略原始网格。如果所有进行静态批处理的网格都各不相同,那么与正常渲染对象相比,这不会增加内存使用量。

但是,通常渲染一万个相同对象,消耗的内存是相同的,因为引用相同的网格数据。在这种情况下,对象之间的唯一区别是每个对象的变换。然而静态批处理的引用会丢失,所以使用静态批处理渲染1000个相同的树对象,消耗的内存是不使用的1000倍。会导致严重的内存消耗。

在运行时实例化静态网格

如果需要动态实例化,或者使用叠加方式加载场景,可以使用StaticBatchUntility.Combine() 两个重载形式:

  1. 需要提供根GameObject,该对象中所有带网格的子GameObject对象都会转换到新的静态批处理组中(如果使用了多个材质,就会创建多个组)。
  2. 需要提供GameObject列表和一个根GameObject,该重载形式会自动将列表中的对象作为根对象的子节点,以相同的方式生成新的静态批处理组。
    注意:如果有许多顶点合并,性能开销会非常昂贵。并且它无法和任何预先存在的静态批处理合并

静态批处理总结

缺点:强大但危险的工具。很容易造成内存消耗(导致应用程序崩溃)。还需要大量手动调整和配置,以确保正确生成批处理。
优点:可以使用不同形状和巨大尺寸的网格,这是动态批处理无法提供的。
在这里插入图片描述

3. GPU Instancin

在使用相同材质球、相同Mesh(预设体的实例会自动地使用相同的网格模型和材质)的情况下,Unity会在运行时对于正在视野中的符合要求的所有对象使用Constant Buffer[5]将其位置、缩放、uv偏移、lightmapindex等相关信息保存在显存中的“统一/常量缓冲器”[6]中,然后从中抽取一个对象作为实例送入渲染流程,当在执行DrawCall操作后,从显存中取出实例的部分共享信息与从GPU常量缓冲器中取出对应对象的相关信息一并传递到下一渲染阶段,与此同时,不同的着色器阶段可以从缓存区中直接获取到需要的常量,不用设置两次常量。

不严谨的人话:GPU多例化技术原理简单,就是先把数据提交到显存中,然后在绘制时,修改一些属性,就能达到避免N次设置渲染状态造成的性能消耗。不同的物体靠参数属性的不同进行区分。

GPU Instancin应用场景

在某些场合是我已知唯一的处理优化手段。比如几千棵树,几万株草的草地。

GPU Instancin优点

GPU Instancing在减少DC的同时,也避免了内存的爆炸。
2018版本已经支持GI,不用再手写了。

GPU Instancin缺点

操作过多。如果使用自定义Shader,则需要修改代码
由于constant buffer的限制(默认是500),DC数量也会缓慢增加

4. SRP Batcher

在使用LWRP或者HWRP时,开启SRP Batcher的情况下,只要物体的Shader中变体一致,就可以启用SRP Batcher加速。它与上文GPU Instancing实现的原理相近,Unity会在运行时对于正在视野中的符合要求的所有对象使用“Per Object” GPU BUFFER(一个独立的Buffer) 将其位置、缩放、uv偏移、lightmapindex等相关信息保存在GPU内存中,同时也会将正在视野中的符合要求的所有对象使用Constant Buffer[5]将材质信息保存在保存在显存中的“统一/常量缓冲器”[6]中。
(LWRP是老版本的叫法,新的忘记了)

总结

批处理可以合并不同的mesh,而GPU Instancing主要是针对同一个mesh。

当物体顶点数量级别较大,重复度较低时,选择静态批处理(场景模型);
当物体顶点数较少且数量较少时,可以按需求选择动态批处理,当超过一定级别数时(参考:20个顶点的物体,数量超过3000)选择GPU Instancing将更有优势,且数量越多,优势越明显。

参考资料

必读: http://newhappy.com.cn/index.php/2020/05/14/batch/
必读:关于静态批处理/动态批处理/GPU Instancing /SRP Batcher的详细剖析
必读:利用GPU实现大规模动画角色渲染

1.《Unity游戏优化》第二版
2. 官方网站文档-批处理
3. 官方文档-GPU多例化
4. GPU多例化

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值