batching(合批) 和大量的描述一个3D物体的数据有关系,比如meshes,verices,edges,UV coordinates 以及其他不同类型的数据。在Unity中谈论batching,指的是用于合批mesh数据的两个东西:Dynamic Batching(动态合批) 和 Static Batching(静态合批)。渲染管线特别喜欢处理合在一起提交的mesh数据而不是单独的一个个的mesh数据。
在讨论Dynamic Batching(动态合批) 和 Static Batching(静态合批)之前,先理解下Dynamic Batching(动态合批) 和 Static Batching(静态合批)要解决渲染管线的问题:降低用于绘制当前视图所有物体的Draw calls。
Draw call是从CPU发送到GPU的一个请求,用于物体的绘制。在Draw call请求之前,需要完成一系列的准备工作。
首先,mesh和textrue数据必须从CPU 的内存(RAM)推送到GPU内存(VRAM),对于已经存在于场景里的,这通常发生在场景初始化的时候。对于预先没有在场景内的,只能在它们被instantiate的时候进行该操作。其次,cpu必须为gpu准备渲染所需要的设置和数据。cpu和gpu之间的通信是通过不同平台的图形API来完成的,有可能是DirectX,OpenGL,Vulkan等等。
API通过驱动程序调用,大量繁杂的渲染管线渲染物体所需的设置信息通常被叫做Render State。在Render State改变之前,GPU都将会一直使用这个Render State来渲染物体。对Render State做改变是一个开销非常大的过程,比如我们设置Render State使用一张蓝色texture去渲染mesh,这会将整个mesh渲染成蓝色,我们可以再渲染9个完全不同的mesh,它们都会被渲染成蓝色,因为我们没有改变使用的texture。但是如果我们想渲染10个mesh哟个10张不同的贴图,就会十分耗时,因为需要渲染每个mesh时都去准备新的贴图信息发送draw call。越少更改Render State,图形api处理我们的请求就会越快。
能触发Render State同步的操作包括但不限于:添加实时的texture到GPU,修改Shader,修改灯光信息,修改阴影和透明度等等有关渲染的设置。
CPU和GPU之前通过Command Buffer保持通信,这个队列存储了CPU创建好的用于渲染的操作指令,GPU在每次完成渲染指令后再从这个队列中获取。
一个新的Draw Call并不意味着必须有一个新的Render State。合批能够提供渲染性能的原因是如果俩个物体使用同样的RenderState信息来渲染,GPU就可以立刻渲染下一个物体,这消除了Render State的同步时间,合批同样可以减少渲染的指令数量,减少CPU和GPU的负荷。
Unity中通过Materials来把Render State暴露给我们。Materials是一个Shader的容器,shader本身并不感知实际的Render State,shader所需的diffuse textures,normal maps等实际就是隐式的Render State变量。
每一个shader都需要一个Material,一个Material也必须有一个Shader。因此如果我们想减少Render State的变化,我们可以通过减少场景中Materials的数量。CPU每帧会消耗更少的时间来生成和传输渲染指令,GPU也不需要频繁停下来重新同步Render State的变化。
做个实验,四个cube,四个sphere,各自不同的material,关掉shadow,关掉dynamic batching 和 static batching:
结果为9个batches(还有一个是背景,比如天空盒)
像之前介绍的那样,我们可以减少material数量来提高效率,如果都用同样的material会是什么效果:依然还是9个,这是因为我们没开启Dynamic Batching,渲染管线并不能够意识到需要重复使用Render State。
Unity中调试渲染非常有用的工具,可以查看每一帧的渲染过程,对于跟踪场景中DrawCall来源,优化游戏非常有用。这一小节就不详细介绍了
三个特性:
1. 运行中实时动态合批
2. 根据主摄像机视野中可见的物体,每一帧合批的内容都可以不同
3. 运动的物体也可以进行合批
上一小节的例子,开启动态合批后结果为:6个batches,这是因为4个Sphere因为不满足动态合批条件而没有进行合批。
动态合批的条件具体参考Unity的文档,这玩意随着Unity版本的变化不断在变化。
https://docs.unity3d.com/Manual/DrawCallBatching.html
动态合批需要满足的条件:
顶点属性是每个顶点的一系列信息,比如位置,法线,UV等。如果每个顶点3个属性,则如果想合批,可以最多支持的顶点数是900/3 = 300。如果每个顶点5个属性,则最多可以持的顶点数是900/5 = 180。要注意的是即使顶点属性少于3个,合批最多也只能支持300个顶点(另外一条规则)
奇数个负数Scale不能合批,比如(1,-1,1),偶数个可以(1,-1,-1)
Unity 提供了第二种合批方式,那就是Static Batching。Static Batching的条件:
要小心如果只是把物体标记为Batching Static,运行时可能会产生奇怪的现象:mesh由于Static Batching没有移动,但是Rigidbody什么的可能会动。
额外的内存开销是Static Batching的一个缺点,Static Batching copy 需要合批的mesh的数据到一个单独的大mesh buffer中,并将其传入渲染管线中通过一个Draw Call进行渲染,完全忽略掉原始的mesh。如果需要合批的mesh都是不同的,那是否使用静态合批内存的开销是一样的。然而如果渲染的是相同的object,则内存开销会翻倍,平常渲染一个,10个或者100万个同样object的clone花费的内存开销是一样的,因为他们引用的是相同的mesh 数据,对于每个Object唯一不同的只是transform。然而Static Batching 需要copy mesh 数据到大的buffer中,这种引用关系就没有了,每个物体都会将原始的mesh数据copy到buffer中。因此使用Static Batching 渲染1000个tree的内存开销是不使用Static Batching的1000倍,所以在使用Static Batching时要慎重,要判断使用的是否合适。
有时候,Static Batching会需要处理多个Materials,在这种情况下,每个Material会被分组到各自的Static Bathc中,也就是说Static Batching可以用和Materials数量相等的Draw Call渲染所有的静态meshes。
Static Batching 是一个十分有用但是又十分危险的工具,如果使用的好会给渲染带来提升,如果使用的不好,内存的开销以及各种其它弊端都会很蛋疼。
Dynamic Batching 和 Static Batching summary都是有用但是又有风险的工具,要真正搞明白它们才能正确的使用它们。第六章会更多更深入的介绍Dynamic Graphics。
此处加一点书中这章没写的内容,GPU Instancing也会有效降低DrawCall,它的原理详见
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/
GPU Instancing 非常高效,它的要求也更高,不仅要求Material引用一样,mesh也必须一样,因为mesh一样所以才不会存在Batching合并大mesh带来的内存开销。
Unity Batching优先级:静态合批 > GPU Instancing > 动态合批