Unity引擎源码札记(一):Dynamic Batching
1. 概述
本文档研究Unity的动态合批(Dynamic Batching)特性,修改引擎源码(基于Unity 4.6)进行性能分析,讨论实现引擎底层定制化功能可行性。文档背景在于进一步理解Dynamic Batching,探索引擎底层做定制优化的可能性,在此做个记录。
2. Dynamic Batching
Dynamic Batching条件
简单地说,Dynamic Batching是自动进行的,只要对象使用相同的材质并符合如下规则,就会组成Batch。官方文档给出的条件如下:
(http://docs.unity3d.com/Manual/DrawCallBatching.html):
1)顶点个数:由于物体顶点太多开销会过大,因此仅适合小于900个顶点的Meshes,如果你的Shader有使用Vertex Position,Normal或是Single UV,顶点数下降到300;如果还有UV0 UV1 Tangent,那只能处理180顶点(这是参考值,会随着版本演化而改变);
2)对于缩放来说, 等比缩放和非等比缩放的物体会分开处理;
3)使用不同材质的实例化游戏对象,会导致批处理失败;
4)拥有lightmap的对象,含有额外的渲染属性,例如lightmap的索引、偏移以及缩放等系数;
5)如果物体Shader有Multi-pass会中止批量处理;
6)物体接收实时阴影不会纳入批处理。
部份动态合拼条件源码:
阈值:
Vertex Indices threshold
Dynamic batching过程中,会计算下一个mesh需要的顶点索引数目,如果vertex index 超过32000(引擎源码中kDynamicBatchingIndicesThreshold)时,将停止当前批次。比如,假定物件A符合dynamic batching的条件,共有有45个实例,第45个实例合批时,vertex index 已超过32000,将生成另外一个批次绘制其余实例。参考下图对比,场景中增加第45个wp_l_32实例时,Draw call数目增加1,saved by batching没有变化。(注:Statistics窗口显示的统计数据,其中一个draw call为相机clear, 下同。)
CPU开销
动态合批从源码上看,是会带来一定的CPU开销的,下面通过测试分析增加顶点数阈值的可行性。
默认引擎动态合批的顶点数阈值(kDynamicBatchingVerticesThreshold)为300,修改为500,对比修改前后同一场景的性能消耗。
两个模型:A( wp_l_032) 与 B( wp_l_033) 顶点数分别为210和411。并且放置同一场景中,有200个A实例,50个B实例。
引擎默认kDynamicBatchingVerticesThreshold =300时,draw call为56,除开相机clear需要1个draw call,200个A合并为5个draw call进行绘制,50个B需要50个draw call(顶点数>300,不符合动态合批)。
修改kDynamicBatchingVerticesThreshold =500时,draw call为8,除开相机clear需要1个 draw call,200个A合并为5个draw call进行绘制,50个合并为2个draw call(顶点数<500,符合动态合批)。
可以看出修改后draw call 大大减少了,减轻了GPU负载。以下使用WeTest对该场景进行真机测试(引擎为release版本)。
测试环境:
测试数据:
可以看出,threshold=300与threshold=500两种情况下,内存占用相似,但后者的CPU占用率比前者高出不少,FPS反而因此略有下降。因而,Dynamic batching并非尽可能多用,视具体情况分析。如果GPU负载品瓶颈,CPU负载又相对宽裕,可以尽可能符合dynamic batching条件或定制修改threshold,扩大合并批次的mesh范围;如果GPU负载尚可,CPU负载过高,则不建议修改threshold。
项目(前期)场景资源统计数据
对项目中,常见场景的mesh资源及默认threshold下draw call情况做采样统计,数据如下:
顶点数 | Scene1 | Scene2 | Scene3 | Scene4 | Scene5 | Scene6 | Scene7 | Scene8 |
0-300 | 263 | 154 | 99 | 145 | 126 | 129 | 138 | 163 |
300-400 | 39 | 6 | 8 | 6 | 11 | 14 | 14 | 1 |
400-500 | 31 | 9 | 11 | 7 | 23 | 17 | 11 | 7 |
500-600 | 6 | 8 | 5 | 4 | 10 | 16 | 12 | 1 |
600-700 | 5 | 4 | 10 | 2 | 5 | 4 | 6 | 2 |
700-800 | 7 | 8 | 2 | 2 | 7 | 11 | 5 | 12 |
800-900 | 1 | 6 | 7 | 4 | 0 | 4 | 3 | 20 |
900-1000 | 2 | 5 | 5 | 6 | 9 | 13 | 6 | 4 |
1000+ | 44 | 56 | 50 | 37 | 38 | 56 | 42 | 81 |
Draw call | 296 | 125 | 111 | 176 | 148 | 152 | 136 | 130 |
Save by batching | 186 | 11 | 12 | 8 | 18 | 0 | 13 | 4 |
数据:
l 大多数场景下300顶点以下,1000顶点以上的对象居多;300顶点以下的对象占50%以上,500顶点以下的对象占60%以上,最多可以占到83%(多人战斗);
l 动态合批节省的draw call ( Save by Batching ) 多数场景下并不多,是因受限于材质、非等比缩放等因素;多人战斗情况下,特效、装备等小物件较多,因此动态合批的作用较大;
结合以上情况,默认的threshold=300能覆盖50%的对象,若将threshold改为500能覆盖60%以上的场景对象,但实际意义不大,节省少量的draw call的同时,会增加CPU负载。
3. NonUniformScale
如果mesh使用非等比缩放,将会生成一份Scaled Mesh。
非等比缩放的动态对象(如每帧更新缩放值曲线的特效对象)会导致的VBO update明显增多,在Adreno Profiler里显示heavy call(下图红色部分)。如下图,某技能特效,使用非等比缩放时,VBO Data Updates为 17,优化后不使用非等比缩放为3,heavy call也大在减少,有利减轻GPU负载。
4. 小结
综上,在资源上应尽量符合Unity官方文档给出的Dynamic Batching的条件,特别是避免非等比缩放,不仅无法动态合批,对于每帧更新的非等比缩放对象,会造成大量VBO Updates,影响性能。另外,动态合批也会增加CPU性能消耗,从引擎底层加大阈值并非好的解决方案,应结合项目当前实际性能状况。