Unity的绘制调用批处理(Draw call batching)

概述

要在屏幕上绘制一个物体,引擎必须发送一个绘制命令(Draw Call)给图形的API,比如OpenGL或者Direct3D的接口。由于每次Draw Call,CPU要准备大量的数据,往往会引起较大的CPU的性能消耗。因此我们需要想办法减少Draw Call的数量。

一般情况下,Unity有两种比较方便使用的减少Draw Call的方法:动态批处理和静态批处理。
动态批处理:适用于面数比较小的物体,Unity会动态的将这些小物体的网格合并在一起,然后一次性绘制
静态批处理:将静态的物体的网格合并成一个大网格,然后一次性绘制

相比手动的去合并Mesh,Unity的合批有一个最大的优点就是仍然可以进行单个对象的剔除,当然也有缺点,比如静态批处理会增加内存的消耗并且需要额外的存储,动态批处理由于需要动态的合并Mesh,需要额外的CPU消耗。

我们可以通过Unity的设置开关批处理
在这里插入图片描述

批处理的材质设置(一些注意点)

只有用相同材质球的物体才能够被合批处理,所以我们想要更好的批处理效果,就需要尽可能的让不同的物体使用相同的材质球。

如果我们的材质球之间的区别仅仅是需要使用不同的贴图,那么我们就可以将这些贴图合并到一张大贴图中来实现使用同一个材质球的需求。我们一般用的UI图集就是这么处理的,所以我们的UI有那么多的绘制对象却只需要较少Draw Call。

如果我们需要在代码里面修改材质,要注意不能用Renderer.material接口,这个会导致复制一份材质实例出来,要用Renderer.sharedMaterial,这样材质球才是被共用的。

阴影的绘制会比较特殊,即使材质不一样,也基本上能合批,只要在阴影的Pass中用到的材质中的参数值是一样的,比如物体用了不同的贴图,但由于阴影Pass中不需要使用贴图,所以它们的阴影还是可以合批的
(我觉得Unity的文档有点坑,讲的一点都不清楚,很可能写文档的人自己都没搞明白,阴影的合批依然的分静态合批和动态合批来讨论,这样子说个大概有啥用)

未设置静态合批时,会尝试阴影动态合批,但仍然有诸如顶点数量的限制,如下:
静态合批
在这里插入图片描述
将这些物体全部设置为静态后,有部分使用了不同ShadowPass的物体依然无法合批
在这里插入图片描述

动态合批(Dynamic batching)

Unity可以对满足条件的相同材质的移动物体进行动态的合批,并且这个合批只需要开启即可,不需要我们做额外的工作。
当然,看起来这么好的事情肯定会有很多限制

动态合批的限制

顶点数量限制

由于要动态的将物体进行合并为一个Mesh,所以合并的物体的顶点数量肯定不能很多,否则合并的性能消耗太大,反而得不偿失
顶点数量不超过300
顶点包含的属性数量不超过900
举个例子:
如果我们的Shader使用到了顶点位置、法线和一套UV,那这里会有三份属性,900/3 = 300,因此顶点数量不能超过300
如果我们的Shader使用到了顶点位置,法线,UV0,UV1,切线,总共五份属性,900/5 = 180,因此顶点数量不能超过180

Scale不允许有负值

其他条件满足后,Scale只要是正值,是多少都可以合批,但只要有任意的负值,这个物体就不再能被合并
所以为了动态合批,我们应该尽量避免设置负的scale
Position和Rotation不会有影响
我们可以简单做个测试

  1. 创建4个Cube
    在这里插入图片描述
  2. 对他们分别做下调整
    左侧两个Y轴是-1
    右侧两个scale都是正值,只是x轴和y轴进行放大
    在这里插入图片描述
    我们来看合批的结果
    我们看到虽然两个scale负值都是-1,但两个都无法合批
    而另外两个虽然scale值不同,但是都是正值,所以也可以合批
    在这里插入图片描述

必须是同一个材质实例

如果是不同的材质实例,即使他们所有的参数都一模一样,也会导致无法动态合批,所以在修改材质时不能用Renderer.material,而是要用Renderer.sharedMaterial
但阴影的合批会比较特殊,主要看用到的参数是否一致

我们创建一个材质,然后复制几份出来,这样他们是一模一样的,但是他们无法合批,我们看到FrameDebug中提示使用了不同的材质导致无法合批
在这里插入图片描述
我们再简单写个脚本测试下修改材质

public class MaterialSet : MonoBehaviour
{
    public bool isSetShareMaterial;
    void Start()
    {
        var renders = GetComponentsInChildren<Renderer>();
        foreach (var render in renders)
        {
            if (isSetShareMaterial)
            {
                render.sharedMaterial.color = Random.ColorHSV();
            }
            else
            {
                render.material.color = Random.ColorHSV();
            }
        }
    }
}

当我们用render.material
修改时,会产生多个材质实例,会有多个颜色,但是无法合批
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
当我们使用
render.sharedMaterial
最终实际只有一种颜色,但是能够动态合批
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

不允许设置额外的光照贴图渲染参数

设置了lightmap Index和Offset/Scale的无法进行动态合批,这个设置更多的是静态烘培后会有的设置,所以更适合用静态合批,如果设置了的话,动态合批就无法设置了

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LightmapCopy : MonoBehaviour
{
    public MeshRenderer meshRenderer;
    void Start()
    {
    }

    public void OnValidate()
    {
        if (meshRenderer == null)
            return;
        var myRenderer = GetComponent<MeshRenderer>();
        myRenderer.lightmapIndex = meshRenderer.lightmapIndex;
        myRenderer.lightmapScaleOffset = meshRenderer.lightmapScaleOffset;
    }
}

我们烘培后,拿几个物体设置为完全一样的贴图设置
在烘培后
在这里插入图片描述
在这里插入图片描述
我们看到无法合批,说明就是不能设置lightmap参数

多Pass的Shader会打断合批

几乎所有的UnityShader都支持多个灯光的前向渲染模式(Forward Rendering),这就要求多个Pass,额外的像素灯光的DrawCall就无法合批了
遗留的延迟渲染会禁用掉动态合批,因为需要绘制物体两次

在不同的平台上动态合批并不一定具有优势

动态合批是通过将所有的物体顶点转换到世界坐标中,然后发送到GPU中,所以只有这个工作量小的时候相比绘制每一个物体才会有优势。一次绘制调用所要占用的资源受到很多因素影响,最主要的就是使用的图形API,比如对于游戏主机或者像Apple Metal的现代图形API,一次绘制调用的性能消耗比较小,这样一来动态合批就没有什么优势了。

其它渲染器的动态合批

对于粒子系统(Particle Systems)、Line Renderers、Trail Renderers这些并不是由Mesh构成,而是Unity动态生成的带有几何图形的组件,它们的动态合批方式和Mesh的不一样
合批过程
1. 对于能够合批的Renderer,Unity会将所有的合批内容合并到一个大的顶点缓冲(Vertex Buffer)中
2. 渲染器设置这个绘制调用批次的材质属性
3. Unity将顶点缓冲绑定到图形设备(Graphics Device)上
4. 对于一个批次中的每个渲染器,Unity会更新顶点缓冲中的偏移,然后提交一个新的绘制调用

(说实话,是官方文档写的不好,还是我学的不好,哪怕有中文的文档了,也搞不太懂…)
当我们去测试图形设备调用的消耗时,渲染一个组件最慢的部分是设置材质的状态,相比之下,提交顶点缓冲中的不同的渲染器的偏移是相当的快。
这个方法与Unity在使用静态批处理时提交绘制调用的方式非常相似。

静态批处理

静态批处理可以合并任意大小的Mesh以减少绘制调用,不过得是使用相同的材质,并且不能移动。由于不需要在CPU上转换顶点,因此静态批处理比动态批处理效率更高,但是需要更多的内存
在这里插入图片描述
在这里插入图片描述
要使用静态批处理很简单,就是把物体检视面板上的Static勾上(最主要是其中的Batching Static),但是勾上后位置、旋转、大小在运行后都无法修改,因为Mesh被合并成一个静态的大Mesh了,所以我们得确定这些物体确实不需要位移、旋转或者大小变化

要静态合批的对象必须使用的是相同的顶点属性,比如顶点位置,顶点法线,和一套UV的对象可以合批,但是这种对象和顶点位置、顶点法线,两套UV、顶点切线的对象就无法合批了。(总觉得Unity的文档连表达都不清楚,上面说了得是相同的材质,为啥还要扯这些东西,难道还有相同材质使用不同顶点属性的?那上面还说相同材质,真实醉了)

Unity不能静态合并具有镜像Transform的对象(这个有问题)
根据我的测试不管是否镜像,不管什么大小,都可以静态合批…(越仔细的看Unity文档,怎么感觉越多问题,究竟是我做的不对,还是怎么回事?)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

两个Cube相互大小镜像,但能静态合批

使用静态批处理需要存储合并的Mesh,如果几个物体在静态合批之前用的是相同的Mesh,但在合批之后每个物体的Mesh都相当于多了一份,因为会生成一份大的Mesh。所以使用静态批处理并不一定是最好的方法,有时候我们不得不不做静态合批,牺牲一些渲染性能让内存占用更小一些。比如在一些有茂密树的关卡中,如果这些树要静态批处理,就会有严重的内存占用。

静态批处理内部原理是通过将静态的物体转换到世界坐标并且为它们构建一个共同的顶点和索引缓冲,如果我们开启了PlayerSettings中的Optimized Mesh Data选项,Unity在构建顶点缓冲时会移除没有使用到的顶点属性(必须是所有的Shader变体都没有使用)。有一些特殊的关键字(Keyword)用来做这个检查,比如Unity如果没有检测到LIGHTMAP_ON关键字,就会在批处理中删除光照贴图UV。然后,对于同一个批次中的可见物体,Unity会执行一系列简单的绘制调用,这些绘制调用之间几乎没有状态的变化。技术上来说,Unity不会减少图形API的调用,但是减少了调用之间的状态变化,而这些状态变化正是消耗大的部分。静态合批有顶点和索引的数量限制,在大部分平台上是64k的顶点,64k的索引,在OpenGLES上索引限制是48k,在macOS上索引限制是32k。

在当前,仅仅Mesh Renderers,Trail Renderers,Line Renderers,Particle Systems和Sprite Renderers能够合批,也就是说,Skinned Meshes,Cloth和其它类型的渲染组件没办法合批。
合批只能在相同的Renderer之间进行

对于透明物体,为了透明效果,这些物体是按从后往前的顺序进行渲染的,Unity首先会按这个顺序进行排序,然后尝试对它们进行合批,但是因为要保证正确的渲染顺序,所以能进行静态合批的透明物体比不透明物体会更少。

手动的合并比较靠近的物体是一个合并DrawCall比较好的替代方法,在功能允许的情况下,将能合并的物体都手动合并到一个Mesh中,比如一个静态的柜子,有许多抽屉,这些抽屉就能和柜子一起合并为一个Mesh。我们可以通过其它的3D软件或者通过Mesh.CombineMeshes来实现手动合并。

我的总结

Unity的这个合批说明只能了解个大概,大概知道有哪些限制,大体是怎么使用,但Unity文档自己都没讲清楚,要嘛偷懒,要嘛就是写文档的人自己都不清楚,所以真要想优化合批,还是得自己在项目里实践,最主要的工具就是通过FrameDebuger分析,里面会有具体的不能合批理由,这个非常实用
在这里插入图片描述
在这里插入图片描述

Unity版本

Unity2020.3(LTS)

本文

https://blog.csdn.net/ithot/article/details/122262314?spm=1001.2014.3001.5502

本文测试工程

https://github.com/MonkeyTomato/DrawCallBatchingTest

参考

本文主要根据Unity的官方文档整理而来
https://docs.unity3d.com/Manual/DrawCallBatching.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值