batching(合批) 和大量的描述一个3D物体的数据有关系,比如meshes,verices,edges,UV coordinates 以及其他不同类型的数据。在Unity中谈论batching,指的是用于合批mesh数据的两个东西:Dynamic Batching(动态合批) 和 Static Batching(静态合批)。渲染管线特别喜欢处理合在一起提交的mesh数据而不是单独的一个个的mesh数据。
Draw Calls
在讨论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的负荷。
Materials and Shaders
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。
The Frame Debugger
Unity中调试渲染非常有用的工具,可以查看每一帧的渲染过程,对于跟踪场景中DrawCall来源,优化游戏非常有用。这一小节就不详细介绍了
Dynamic Batching
三个特性:
1. 运行中实时动态合批
2. 根据主摄像机视野中可见的物体,每一帧合批的内容都可以不同
3. 运动的物体也可以进行合批
上一小节的例子,开启动态合批后结果为:6个batches,这是因为4个Sphere因为不满足动态合批条件而没有进行合批。
动态合批的条件具体参考Unity的文档,这玩意随着Unity版本的变化不断在变化。
https://docs.unity3d.com/Manual/DrawCallBatching.html
动态合批需要满足的条件:
- 使用相同的材质引用
- 只有ParticleSystem和MeshRender可以合批,SkinnedMeshRender等其他Component无法合批.Currently, only Mesh Renderers, Trail Renderers, Line Renderers, Particle Systems and Sprite Renderers are batched. This means that skinned Meshes, Cloth, and other types of rendering components are not batched.
- 每个mesh最多300个顶点索引。
- 最多900个顶点属性 。
- mesh的Scale要么都统一,要么都别统一(这句没看懂,令人费解)
- 材质的shader不能使用多passes
- mesh不能接收实时阴影
- 合批后mesh总共的索引数不能超过各自平台的上限,一般是32k-64k个。
Vertex attributes
顶点属性是每个顶点的一系列信息,比如位置,法线,UV等。如果每个顶点3个属性,则如果想合批,可以最多支持的顶点数是900/3 = 300。如果每个顶点5个属性,则最多可以持的顶点数是900/5 = 180。要注意的是即使顶点属性少于3个,合批最多也只能支持300个顶点(另外一条规则)
Mesh scaling
奇数个负数Scale不能合批,比如(1,-1,1),偶数个可以(1,-1,-1)
Dynamic Batching summary
- 适用于场景中含有大量简单的物体,比如大片有石头和树木的森林等。
- 如果只是单纯的texture不同,可以考虑使用图集的方式,这样就可以合批。
- 如果场景中有很多个比较简单的物体,但是只有少数可以合批的话,还不如不开启合批,因为遍历场景判断合批的操作可能更费。
- 动态合批的条件非常苛刻,经常会出现打破了合批规则而自己不知道,所以要多注意,要经常不定期的去调试check来保持Draw call的合理值,当然,常常的状况是我们直到Draw Call真的成为性能瓶颈影响表现时,并不需要去考虑Draw call。
- 最后一条建议,多尝试,看看哪些不能合批哪些能合批来保证Draw call
Static Batching
Unity 提供了第二种合批方式,那就是Static Batching。Static Batching的条件:
- 物体必须被标记为Static
- 需要额外的内存
- 合批后mesh总共的索引数不能超过各自平台的上限,一般是32k-64k个。
- mesh可以不同,但是必须使用相同的材质引用
The Static flag
要小心如果只是把物体标记为Batching Static,运行时可能会产生奇怪的现象:mesh由于Static Batching没有移动,但是Rigidbody什么的可能会动。
Memory requirements
额外的内存开销是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时要慎重,要判断使用的是否合适。
Material references
有时候,Static Batching会需要处理多个Materials,在这种情况下,每个Material会被分组到各自的Static Bathc中,也就是说Static Batching可以用和Materials数量相等的Draw Call渲染所有的静态meshes。
Static Batching caveat & Edit Mode debugging of Static Batching & Instantiating static meshes at runtime
- Static Batching 不能在编辑器中立刻查看效果,只能运行起来后看效果,调试起来非常麻烦
- 运行时将物体动态标记成Static并不会被合批,但是可以通过 StaticBatchUtility.Combine() 强制合批,但是要非常谨慎使用,这个函数开销极大
Static Batching summary
Static Batching 是一个十分有用但是又十分危险的工具,如果使用的好会给渲染带来提升,如果使用的不好,内存的开销以及各种其它弊端都会很蛋疼。
Summary
Dynamic Batching 和 Static Batching summary都是有用但是又有风险的工具,要真正搞明白它们才能正确的使用它们。第六章会更多更深入的介绍Dynamic Graphics。
GPU Instancing
此处加一点书中这章没写的内容,GPU Instancing也会有效降低DrawCall,它的原理详见
https://learnopengl-cn.github.io/04%20Advanced%20OpenGL/10%20Instancing/
GPU Instancing 非常高效,它的要求也更高,不仅要求Material引用一样,mesh也必须一样,因为mesh一样所以才不会存在Batching合并大mesh带来的内存开销。
Unity Batching优先级:静态合批 > GPU Instancing > 动态合批