合批是什么?为什么可以减少Drawcall?有什么合批方法?
发现自己只是知道合批怎么用,是可以减少drawcall,但却不知道这些底层的机制是怎么样的,为什么可以减少drawcall?这可是犯了大忌!决定潜心学习研究一下,在此记录。
首先什么是合批?
合批,也可以叫做批量渲染。合批就是通过减少CPU向GPU发送渲染命令(DrawCall)的次数,以及减少GPU切换渲染状态的次数,尽量让GPU一次多做一些事情,来提升逻辑线和渲染线的整体效率
但参与合批有个前提,就是参与合批的材质必须相同!
那么为什么合批可以优化drawcall?
让我们先看一个drawcall所走过的流程需要哪些消耗:
Application -> Runtime -> Driver -> 显卡(GPU)
Runtime会将所有API调用先转换成与设备无关的命令(这里为什么说是与设备无关的命令,是因为这样我们写的程序就可以运行在任何特性兼容的硬件上了),而Drawcall性能消耗原因是命令从Runtime到Driver的过程中,CPU要发生从用户模式到内核模式的切换,这个模式切换对于CPU来说是个非常耗时的工作。
那么如果Runtime中有一个Command Buffer可以将一些没必要马上发送给Driver的命令缓存起来,在适当的时机一起发送给Driver,再在GPU里执行。这样是不是就减少了CPU的模式切换,从而提升了效率呢?没错,这就是合批可以干的事!
让我们来看看有什么合批
离线合批
利用美术工具例如Maya把相关资源做合批处理,以减轻引擎实时合批的负担。比如静态模型和场景物件。如场景地表装饰面:石头/砖块等等
静态合批
对标记为static的Mesh自动合批。以空间换时间的策略来提升渲染效率。以存储更多网格数据为代价的
如果在使用相同材质球的条件下,在Build的时候Unity会自动地提取这些共享材质的静态模型的Vertex buffer和Index buffer。根据其摆放在场景中的位置等最终状态信息,将这些模型的顶点数据变换到世界空间下,存储在新构建的大Vertex buffer和Index buffer中,并且记录每一个子模型的Index buffer数据在构建的大Index buffer中的起始(start0)及结束(end0)位置。
在后续的绘制过程中,一次性提交整个合并模型的顶点数据,根据引擎的场景管理系统判断各个子模型的可见性。然后设置一次渲染状态,调用多次Draw call分别绘制每一个子模型。所以其实static batching是不减少Draw call的数量(但是在编辑器时由于计算方法区别Draw call数量是会显示减少了的),但是由于我们预先把所有的子模型的顶点变换到了世界空间下,所以在运行时cpu不需要再次执行顶点变换操作,节约了少量的计算资源,并且这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API(Command Buffer)会缓存绘制命令,起到了渲染优化的目的 。
但静态合并有个很大的缺点就是打包之后会导致应用体积增大,应用运行时所占用的内存体积也会增大。例如,在茂密的森林级别将树标记为静态会严重影响内存,因为场景中所有引用相同模型的GameObject都必须将模型顶点信息复制,并经过计算变化到最终在世界空间中,存储在最终生成的Vertex buffer中,这个时候的vertex buffer会特别大。
动态合批
将数份Mesh的数据复制粘贴到一起,也就是实时的,每一帧都合并,但不适用于网格数据太多的物体(比如球)
在进行场景绘制之前将所有的共享同一材质的模型的顶点信息变换到世界空间中,然后通过一次Draw call绘制多个模型,达到合批的目的。模型顶点变换的操作是由CPU完成的,所以这会带来一些CPU的性能消耗。并且计算的模型顶点数量不宜太多,否则CPU串行计算耗费的时间太长会造成场景渲染卡顿,所以Dynamic batching只能处理一些小模型。所以仅仅在合批操作的性能消耗小于不合批,Dynamic batching才会有意义,虽然在内存占用和发布的程序体积方面要优于Static batching。
无法参加dynamic batching的情况:
- 物件Mesh大于等于900个面
- 代码动态改变材质变量后不算同一个材质,会不参与合批
- 如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只能批处理180顶点以下的物体,否则都无法参与合批
这个与static batching的区别主要网格信息是合并在一起了,command buffer只有一次drawcall了
GPU Instancing
使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU -> GPU的通信。也就是,让GPU一次性渲染同一网格多次,每次绘制的网格属性都可以不一样:包括缩放、位置、颜色等等,即材质球虽然相同但属性可以各有各的区别。
在使用相同材质球、相同Mesh(预设体的实例会自动地使用相同的网格模型和材质)的情况下,Unity会在运行时对于正在视野中的符合要求的所有对象使用Constant Buffer将其位置、缩放、uv偏移、lightmapindex等相关信息保存在显存中的“统一/常量缓冲器”中,然后从中抽取一个对象作为实例送入渲染流程,当在执行DrawCall操作后,从显存中取出实例的部分共享信息与从GPU常量缓冲器中取出对应对象的相关信息一并传递到下一渲染阶段,与此同时,不同的着色器阶段可以从缓存区中直接获取到需要的常量,不用设置两次常量
GPU Instancing可以规避合并Mesh导致的内存与性能上升的问题,但是由于场景中所有符合该合批条件的渲染物体的信息每帧都要被重新创建,放入“统一/常量缓冲区”中,而碍于缓存区的大小限制,每一个Constant Buffer的大小要严格限制(不得大于64k)
动态合批与静态合批的区别:
- 动态合批不会创建常驻内存的“合并后网格”,也就是说它不会在运行时造成内存的显著增长,也不会影响打包时的包体大小;
- 动态合批在绘制前会先将顶点转换到世界坐标系下,然后再填充进顶点、索引缓冲区;静态合批后子网格不接受任何变换操作,仅手动合批后的Root节点可被操作,因此静态合批的顶点、索引缓冲区中的信息不会被修改(Root的变换信息则会通过Constant Buffer传入);
- 因为2的原因,动态合批的主要开销在于遍历顶点进行空间变换时的对CPU性能的开销;静态合批没有这个操作,所以也没有这个开销;
- 动态合批使用根据渲染器类型分配的公共缓冲区,而静态合批使用自己专用的缓冲区。
Reference
- https://blog.csdn.net/chenweiyu11962/article/details/121340711
- https://zhuanlan.zhihu.com/p/98642798
- https://zhuanlan.zhihu.com/p/356211912