一、DrawCall、Batches、SetPassCalls的基本理解
我们先从图形渲染的角度对这些概念做一个基本的理解。
1.1 DrawCall
DrawCall实际上指的是一次图形渲染接口的调用,比如OpenGL的glDrawArrays或者glDrawElements的一次调用,以及DirectX的DrawPrimitive或者DrawIndexedPrimitive。因此,DrawCall可以简单理解为一次渲染指令调用。
1.2 Batches
我们知道,在调用DrawCall之前,需要设置渲染状态,比如当前使用的Shader、当前shader的参数(材质参数)、深度测试是否开启、模板测试设置等,设置完这些状态后,才会调用DrawCall。我们把设置渲染状态,加载网格数据,然后调用DrawCall这一个过程,叫做一个批次。理论上,我们可以在设置完渲染状态后,调用多个DrawCall,假如一个DrawCall的绘制数量有限制的话,但是通常一个批次也就调用一次DrawCall。
那么所谓合批,就是想办法尽量减少批次。减少批次的关键是减少场景中不同的渲染状态组合,也就是渲染状态切换尽可能少。这样子批次自然最少。批次少了,批次对应的DrawCall自然少了,每个批次需要的渲染状态切换也少了。注意,渲染状态切换类似于DrawCall都是一次渲染指令调用。
1.3 SetPassCalls
那么什么是SetPassCalls了。在Shader中有一个Pass的概念,比如一个Shader有2个Pass,那么实际上应用这个Shader的物体会按照Shader的Pass定义顺序渲染2遍,每一遍都是用对应的Pass渲染。Unity的官方文档里面解释SetPassCalls就是Shader中的Pass被切换的次数,因为每个渲染批次都会设置一个Pass,一个Pass就会对应一些渲染状态,当渲染状态变化时候就必须开始新的批次,但是新的批次下Pass可能没有变化
二、Unity的DrawCall、Batches、SetPassCalls区别和联系
我们以一个场景运行时的统计数据为例子来说明。我们打开Unity场景的Statistics窗口,
以及Profile窗口,
FrameDebug窗口
我们可以得出这个场景的DrawCall是584,Batches也是584,SetPassCalls是192。Statistics中是不会显示DrawCall的,只有在Profile窗口下选中Rendering才能看到。
2.1 Unity的DrawCall
根据运行数据,可以得出结论DrawCall数目基本等于Batches。为什么说基本了?因为同一个Batch下,可能分多次调用DrawCall,比如网格过于巨大,可能拆分成多个DrawCall,这个也是符合批次的定义的。不过,这种情况发生的可能性很小。
2.2 Unity的Batches
Unity的批次实际上就是前面解释的Batches。不过,Batches实际上包含有三类:Static Batches、Dynamic Batches、Instancing Batches,分别对应Unity的静态合批、动态合批、实例化渲染。
2.3 Unity的SetPassCalls
根据FrameDebug窗口可以看到,一共是197+24+1+1=233个渲染事件。其中,Clear事件有14个。除去Clear事件后还生效219的事件,不过我们的SetPassCalls是192,还多了17个。我们观察到UI相机有18个DrawMesh事件,点击后发现这个事件使用的都是同样的Pass,如下图所示,
这些Pass之间除了材质属性外的渲染状态都是一致的,因此还要减去17。
注意,FrameDebug窗口的截图中折叠的部分基本是SRP Batch。
根据这些数据我们可以得出结论,如果支持SRP Batch,一个SetPassCall等于一个SRP Batch;如果不支持SRP Batch,那么一个SetPassCall就是一次Shader的Pass切换。由于Pass切换实际上指的是Shader关键字或者ROP阶段的设置改变,那么其实这个跟SRP是一致的。SRP本质上也是Shader变体切换,而非传统的材质切换。传统的材质切换对应的是Batches。
注明:实验引擎版本是Unity2020.3.12。
2.4 总结
至此可以得出最终结论,Unity的DrawCall和Batches数目基本相等,对应的是传统的材质切换。SetPassCalls一般会大幅度少于Batches,对应的是SRP Batch或者Pass切换,数目等于FrameDebug中的事件数目减去Clear事件、Draw Mesh事件中重复的Pass数目。
三、DrawCall相关的性能优化
3.1 为什么需要降低DrawCall
一谈起游戏优化,尤其是渲染优化,大家就说降低DrawCall,降低批次。实际上,大部分人都没法正确区分,Unity引擎下DrawCall、Batch、SetPassCall这三个概念。DrawCall或者批次高,并不是性能低下的直接原因,真正的原因是批次高,导致渲染状态切换过多。而渲染状态切换实际上是发生的渲染管线的CPU阶段,使用图形API,比如OpenGL或者DirectX来完成的。这样CPU会花费大量的时间提交渲染指令给GPU,CPU占用过高,但是GPU的渲染指令队列并没有饱和,GPU执行渲染指令的速度很快,因此GPU的负荷可能还没上来,GPU在等待CPU提交渲染指令,整个渲染流水线没有最高速的跑起来。当然如果GPU也忙不过来,那么不仅仅需要降低批次,Shader复杂度和OverDraw应该是重点关注对象。
3.2 如何降低批次
3.2.1 静态合批
静态合批实际上是引擎在打包或者烘焙时候,将同材质的物体合并成一个更大的物体,这样相同材质的物体只需要一次渲染状态设置和一次DrawCall调用,也就一个批次。由于合并生成大的模型后,会占用额外的内存空间,比如三个同材质的立方体的网格就是一个简单的立方体,合并后的网格占用是三个世界空间立方体的组合,因此有时候需要考虑静态合批带来的内存增长。
3.2.2 动态合批
动态合批是静态合批在运行时的体现。Unity对动态合批有一些限制,比如限制模型顶点属性不能超过900等,具体可以参考Dynamic batching。动态合批由于是运行是合并网格,因此不仅会增大内存,还会占用CPU时间。动态合批一般应用在一些小物体的合并上,比如小的道具或者特效等。
3.4.3 Instancing Draw
Instancing Draw实际上是图形接口支持的一种技术,可以翻译为实例化渲染,可以参考文档:实例化。这种技术通常应用在重复的物体大量出现的情况下,比如说草地、树木、星星,这种只有位置或者朝向、缩放等不一样。实例化渲染可以通过指定每物体属性(正常的每顶点属性是每个顶点不一样)来传入这种每个物体不一样的属性,从而避免使用不同的材质。在OpenGL中是使用glVertexAttribDivisor来设置属性的更新速度,从而指定每物体属性。
至于Unity的Instancing,参考文档:GPU instancing。关键点:GPU instancing不能和SRP Batcher并存;GPU instancing不支持 SkinnedMeshRenderers(蒙皮); Graphics.DrawMeshInstanced或者Graphics.DrawMeshInstancedIndirect是主动Instancing,如果不调用这2个函数,那么Unity会尝试Instancing(如果Shader支持Instancing,且没有开启SRP Batch),这会有额外的CPU消耗。
3.4.4 SRP Batcher
参考文档:Scriptable Render Pipeline (SRP) Batcher。关键点:只有可编程管线才支持,默认管线不能支持;Shader必须支持SRP Batcher;只支持Mesh和SkinMesh,不支持粒子系统;不能与 Instancing Draw兼容;如果使用了MaterialPropertyBlock,SRP Batcher无法开启。
SRP Batcher本质上是Shader变体级别的合批优化,根据前面的分析等价于一次SetPassCall。具体原理还是参考Unity 的官方文档。
3.4.5 合批总结
对于目前的可编程管线,优先使用的都是SRP,因此Shader要尽可能兼容SRP Batcher。对于特殊情况,比如渲染草地这种,才需要舍弃SRP Batcher去使用实例化渲染。对于不支持SRP Batcher的Shader,动态合批和静态合批才可能会被开启。动态合并和静态合批都要增大内存,动态合批还会占用CPU,限制条件还非常多。所以,首选SRP Batcher和Instancing。
由于SRP Batcher不能降低DrawCall和Batcher,实际上降低的是SetPassCall,但是静态合批和动态合批可以降低DrawCall和Batcher。所以,在一些低端机器上,Batcher过多可能引起问题的话,还是得切换回传统的静态合批。不过,根据Unity的官方文档:Optimizing draw calls,SRP Batcher跟静态合批是可以结合在一起的,同时需要注意合批的优先级:SRP和静态合批、Instancing、动态合批。
四、参考资料
DrawCall,Batches,SetPass calls是什么?原理?【匠】
The Rendering Statistics window
Unity Profiler中常见的WaitForTargetFPS、Gfx.WaitForPresent 和 Graphics.PresentAndSync
Draw call batching
————————————————
版权声明:本文为CSDN博主「肖远行」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/csuyuanxing/article/details/123005558