合批

版权声明
作者:曾志伟
文章地址:【Unity游戏开发】合批优化汇总 - 知乎 (zhihu.com)

Reference

【Unity游戏开发】合批优化汇总 知乎 (zhihu.com)

U3D优化批处理GPU Instancing了解一下 知乎 (zhihu.com)

正文

前言

动态合批与静态合批其本质是对将多次绘制请求,在允许的条件下进行合并处理,减少 CPU 对 GPU 绘制请求的次数,达到提高性能的目的。

啥是合批?

批量渲染其实是个老生常谈的话题,它的另一个名字叫做“合批”。
在日常开发中,通常说到优化、提高帧率时,总是会提到它。

为啥要合批?

批量渲染是通过减少CPU向GPU发送渲染命令(DrawCall)的次数,以及减少GPU切换渲染状态的次数,尽量让GPU一次多做一些事情,来提升逻辑线和渲染线的整体效率。
但这是建立在GPU相对空闲,而CPU把更多的时间都耗费在渲染命令的提交上时,才有意义。

调用Draw Call性能消耗原因是啥?

我们的应用中每一次渲染,进行的API调用都会经过Application->Runtime->Driver(驱动)->显卡(GPU)[1],其中每一步都会有一定的耗时。

每调用一次渲染API并不是直接经过以上说的所有组件通知GPU执行我们的调用。

Runtime会将所有的API调用先转换为设备无关的“命令”(之所以是设备无关的,主要是因为这样我们写的程序就可以运行在任何特性兼容的硬件上了。运行时库使不同的硬件架构相对我们变的透明。)

Draw Call性能消耗原因是命令从Runtime到Driver的过程中,CPU要发生从用户模式到内核模式的切换。

模式切换对于CPU来说是一件非常耗时的工作,所以如果所有的API调用Runtime都直接发送渲染命令给Driver,那就会导致每次API调用都发生CPU模式切换,这个性能消耗是非常大的。

Runtime中的Command Buffer可以将一些没有必要马上发送给Driver的命令缓冲起来,在适当的时机一起发送给Driver,进而在显卡执行。以这样的方式来寻求最少的CPU模式切换,提升效率。

CPU从Runtime到Driver非常耗时

动图封面

每次调用DC

动图封面

将一些没有必要马上发送给Driver的命令缓冲起来,在适当的时机一起发送给Driver

解决渲染Batch过多的主要方法:

  • 一个是合批,
  • 一个是对Driver进行优化,降低Driver的性能开销。

四种合批技术

静态合批

勾选StaticBatch 减少Drall但是增加内存 增加Mesh数量 前提相同材质

运行时静态合批 使用StaticBatchingUtility.Combine

包之后体积增大,应用运行时所占用的内存体积也会增大。
需要额外的内存来存储合并的几何体。
注意如果多个GameObject在静态批处理之前共享相同的几何体,则会在编辑器或运行时为每个GameObject创建几何体的副本,这会增大内存的开销。例如,在密集的森林级别将树标记为静态可能会产生严重的内存影响。
静态合批在大多数平台上的限制是64k顶点64k索引

动态合批

Unity自动调用 动态合并Mesh优点是不占用内存 前提相同材质

动态合批的原理也很简单,在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗

1,900个顶点以下的模型。
2,如果我们使用了顶点坐标,法线,UV,那么就只能最多300个顶点。
3,如果我们使用了UV0,UV1,和切线,又更少了,只能最多150个顶点。
4,如果两个模型缩放大小不同,不能被合批的,即模型之间的缩放必须一致。
5,合并网格的材质球的实例必须相同。即材质球属性不能被区分对待,材质球对象实例必须是同一个。
6,如果他们有Lightmap数据,必须相同的才有机会合批。
7,使用多个pass的Shader是绝对不会被合批。因为Multi-pass Shader通常会导致一个物体要连续绘制多次,并切换渲染状态。这会打破其跟其他物体进行Dynamic batching的机会。
8,延迟渲染是无法被合批。

GPU Instancing

GPU Instancing 的处理过程是只提交一个模型网格让GPU绘制很多个地方,这些不同地方绘制的网格可以对缩放大小,旋转角度和坐标有不一样的操作,材质球虽然相同但材质球属性可以各自有各自的区别。

就是说在多个地方绘制同一个Mesh实例。

从图形调用接口上来说 GPU Instancing 调用的是 OpenGL 和 DirectX 里的多实例渲染接口。我们拿 OpenGL 来说:

1
2
3
4
5
6
第一个是无索引的顶点网格集多实例渲染,
void glDrawArraysInstanced(GLenum mode, GLint first, GLsizei count, Glsizei primCount);
第二个是索引网格的多实例渲染,
void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei primCount);
第三个是索引基于偏移的网格多实例渲染。
void glDrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices, GLsizei instanceCount, GLuint baseVertex);

这三个接口都会向GPU传入渲染数据并开启渲染,与平时渲染多次要多次执行整个渲染管线不同的是,这三个接口会分别将模型渲染多次,并且是在同一个渲染管线中。

如果只是一个坐标上渲染多次模型是没有意义的,我们需要将一个模型渲染到不同的多个地方,并且需要有不同的缩放大小和旋转角度,以及不同的材质球参数,这才是我们真正需要的。

GPU Instancing 正为我们提供这个功能,上面三个渲染接口告知Shader着色器开启一个叫 InstancingID 的变量,这个变量可以确定当前着色计算的是第几个实例。

有了这个 InstancingID 就能使得我们在多实例渲染中,辨识当前渲染的模型到底使用哪个属性参数。

Shader的顶点着色器和片元着色器可以通过这个变量来获取模型矩阵、颜色等不同变化的参数。我们来看看在Unity3D的Shader中我们应该做些什么:

GPU Instancing实操:Testplus:U3D优化批处理-GPU Instancing了解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

_**UNITY_VERTEX_INPUT_INSTANCE_ID**_

用于在_**Vertex Shader**_输入 / 输出结构中定义一个语义为_**SV_InstanceID**_的元素。



**_UNITY_INSTANCING_CBUFFER_START(name) / UNITY_INSTANCING_CBUFFER_END_**
每个Instance独有的属性必须定义在一个遵循特殊命名规则的Constant Buffer中。使用这对宏来定义这些Constant Buffer。“name”参数可以是任意字符串。



_**UNITY_DEFINE_INSTANCED_PROP(float4, _Color)**_
定义一个具有特定类型和名字的每个_**Instance**_独有的_**Shader**_属性。这个宏实际会定义一个_**Uniform**_数组。



**_UNITY_SETUP_INSTANCE_ID(v)_**
这个宏必须在**_Vertex Shader_**的最开始调用,如果你需要在**_Fragment Shader_**里访问_**Instanced**_属性,则需要在_**Fragment Shader**_的开始也用一下。这个宏的目的在于让_**Instance ID**_在_**Shader**_函数里也能够被访问到。



_**UNITY_TRANSFER_INSTANCE_ID(v, o)**_
在Vertex Shader中把Instance ID从输入结构拷贝至输出结构中。只有当你需要在Fragment Shader中访问每个Instance独有的属性时才需要写这个宏。



_**UNITY_ACCESS_INSTANCED_PROP(_Color)**_
访问每个Instance独有的属性。这个宏会使用Instance ID作为索引到Uniform数组中去取当前Instance对应的数据。(这个宏在上面的shader中没有出现,在下面我自定义的shader中有引用到)。