参考自:
Unity渲染优化的4种批处理:静态批处理,动态批处理,SRP Batcher 与 GPU Instancing - 知乎 (zhihu.com)
合批/批量渲染 (Batch)、实例化Instancing - 知乎 (zhihu.com)
Unity动态合批(Dynamic Batching)与静态合批(Static Batching) - 简书 (jianshu.com)
什么是DrawCall
渲染流水线的第一步是【CPU和GPU之间的通信】,有如下3个步骤:
1. 把数据加载到显存中:把网格和纹理等数据从硬盘加载到显存中(因为显卡对显存的访问速度更快)
2. 设置渲染状态:CPU根据材质球设置渲染状态,比如,使用哪个顶点着色器/片元着色器、光源属性、纹理等
3. 调用Draw Call:准备好上述工作后,CPU就调用一个渲染命令(Draw Call)来告诉GPU可以开始渲染啦。
GPU的渲染能力是很强的,渲染200个还是2000个三角网格通常没有什么区别,渲染速度往往快于CPU提交命令的速度。如果一帧中Draw Call数量太多,CPU就会在“设置渲染状态-提交Draw Call”上花费大量时间,造成性能问题。
①如果多个DrawCall可以合并为1个DrawCall,就可以通过减少DrawCall来优化渲染性能——比如GPU Instancing,Dynamic Batching
②如果有一组DrawCall使用相同的渲染状态,那么对它们进行批处理,CPU就能在“设置渲染状态”上节省时间——比如SRP Batcher,Static batching——Batch(批)即可理解为“使用相同渲染状态的一组DrawCall”
Unity引擎内建了两种合批渲染技术:Static batching(静态合批)和Dynamic batching(动态合批)。
静态合批
静态合批是物体勾选Static后,Unity在Build的时候,会自动生成合并的网格,并将它以文件形式存储合并后的数据,这样在当场景被加载时,一次性提交整个合并模型的顶点数据,根据引擎的场景管理系统判断各个子模型的可见性。然后设置一次渲染状态,调用多次Draw call分别绘制每一个子模型。需要相同材质而且不能发生变换(旋转平移等等)。
标明为 Static 的静态物件,如果在使用相同材质球的条件下,在Build(项目打包)的时候Unity会自动地提取这些共享材质的静态模型的Vertex buffer和Index buffer。根据其摆放在场景中的位置等最终状态信息,将这些模型的顶点数据变换到世界空间下,存储在新构建的大Vertex buffer和Index buffer中。并且记录每一个子模型的Index buffer数据在构建的大Index buffer中的起始及结束位置。
使用方法:场景中选中需要静态合批的物体,在该物体的Inspector窗口中将右上角的Static勾选。
条件:①使用相同的材质球 ②标明为Static的静态物体
适用范围:像生活中的静态物体一样,建筑,树木等,因为计算合并后的数据在场景内就是固定的不会再变动
优势:
网格通常在预处理阶段(打包)时合并,运行时顶点、索引信息也不会发生变化,所以无需CPU消耗算力维护;
若采用相同的材质,则以一次渲染命令,便可以同时渲染出多个本来相对独立的物体,减少了DrawCall的次数。在渲染前,可以先进行视锥体剔除,减少了顶点着色器对不可见顶点的处理次数,提高了GPU的效率。(官方文档中写着,不会减少DC,但是为了开发人员理解所以编辑器上显示减少了)
弊端:
①增大内存开销:合批后的网格会常驻内存,在有些场景下可能并不适用。比如森林中的每一棵树的网格都相同,如果对它采用静态合批策略,合批后的网格基本等同于:单颗树网格 x 树的数量,这对内存的消耗可能就十分巨大了。因此场景加载后,由于场景内有静态合批数据,会使内存变大。
②包体增大;
结论:
静态合批采用了以空间换时间的策略来提升渲染效率,静态批处理不一定减少DrawCall,但是会让CPU在“设置渲染状态-提交Draw Call”上更高效,由于我们预先把所有的子模型的顶点变换到了世界空间下,并且这些子模型共享材质,所以在多次Draw call调用之间并没有渲染状态的切换,渲染API会缓存绘制命令,起到了渲染优化的目的。此外,在运行时所有的顶点位置处理不再需要进行计算,节约了计算资源
静态合批在大多数平台上的限制是64k顶点和64k索引。
动态合批
在运行时Unity自动把每一帧画面里符合条件*的多个模型网格合并为一个,再传递给GPU,
网格的动态批处理旨在优化旧低端设备的性能。在现代消费类硬件上,动态批处理在 CPU 上执行的工作可能大于绘制调用的开销。这会对性能产生负面影响。
*需符合的条件:(2021)
1. 使用相同材质引用的网格实例——只有使用相同材质球的Mesh才可以被批处理。(详见拓展1)
2. 物体之间Transform不能具有镜像关系——比如物体A的scale=+1,物体B的scale=-1,那就不能被批处理。
3. 着色器使用的顶点属性数量不能大于900——比如,漫反射计算需要使用顶点的“位置、法线、UV”这3种属性,所以模型的顶点数不能超过300;如果着色器需要使用顶点的“位置、法线、UV0、UV1和切向量”这5种属性,那就只能批处理180顶点以下的物体。
4. 材质的着色器不能依赖多个过程——多Pass的shader会中断批处理。(详见拓展3)
5. 网格实例应引用相同的光照纹理文件——如果物体使用光照纹理,需要保证它们指向光照纹理中的同一位置,才可以被动态批处理。
使用方法:
- 转到 Edit > Project Settings > Player。
- 在“Other Settings”中,启用“ Dynamic Batching”。
在自定义管线的工程下,会有渲染管线资源,在上面勾选静态合批,引擎就会在可以动态合批的时候自动进行,需要注意的是,在Unity的自定义管线项目里当物体可以同时进行多种合批的时候,合批的优先使用顺序SRPBatcher>静态合批>GpuInstance>动态合批
使用条件:①使用相同的材质球②在视野范围内的物体
适用范围: 移动的,没有进行前几种合批的物体,
动态合批的过程:
①既然不能预先计算,那么可以在运行时,在cpu里直接把可以合批物体的顶点等数据变换到世界空间下
②渲染时,提交合并后的顶点数据,设置一次渲染状态,调用一次DrawCall绘制多个模型(不同于静态合批的多次调用DrawCall,因为可以进行动态合批的物体已经判断过可见性)
动态合批是利用CPU的计算性能消耗来换取渲染状态设置,几何处理和绘制调用的消耗
缺点:消耗CPU的计算性能,当计算消耗大于设置渲染状态等消耗时得不偿失。
动态合批与静态合批的区别:
- 动态合批不会创建常驻内存的“合并后网格”,也就是说它不会在运行时造成内存的显著增长,也不会影响打包时的包体大小;
- 动态合批在绘制前会先将顶点转换到世界坐标系下,然后再填充进顶点、索引缓冲区;静态合批后子网格不接受任何变换操作,仅手动合批后的Root节点可被操作,因此静态合批的顶点、索引缓冲区中的信息不会被修改(Root的变换信息则会通过Constant Buffer传入);
- 因为2的原因,动态合批的主要开销在于遍历顶点进行空间变换时的对CPU性能的开销;静态合批没有这个操作,所以也没有这个开销;
- 动态合批使用根据渲染器类型分配的公共缓冲区,而静态合批使用自己专用的缓冲区。