在正式开始之前,咱们先做个准备工作:创建一个新场景,然后把自带的平行光给删除,讲相机的Clear Flags改为Solid Color。
此时打开Game视图中的Stats面板,可以看到Batches数为1。
(Stats面板上的参数怎么看,以及什么是Batches,可参考之前写的博客《Unity3D客户端项目优化总结之Stats统计面板》《Unity3D客户端项目优化总结之静态批处理Static Batching》)
再说UGUI的合批之前,先看看什么是批处理。
在说批处理之前我们先看看一个普通的3D模型是怎么渲染出来的。
①首先是CPU这边先准备好这个模型的网格、用到的贴图和Shader,然后GPU将网格、贴图、Shader加载到显存里面。
②然后是CPU设置渲染状态
什么是设置渲染状态呢?就是CPU设置渲染这个网格的时候使用哪个Shader,使用哪几张贴图。第①步中我们可能会准备好很多的Shader(如Shader1、Shader2、Shader3),很多张贴图(贴图1、贴图2、贴图3),设置渲染状态这一步的作用就是告诉GPU,接下来你渲染这个网格的时候使用的Shader是Shader1,不是Shader2、Shader3,使用的贴图是贴图1而不是贴图2、贴图3。
也就是说CPU是老大,GPU是小弟,老大说你下次渲染这个模型的时候用这个Shader和这张贴图,那么小弟开始干活的时候就按老大的要求来。
③CPU设置完毕渲染状态后,GPU还没正式开始渲染这个模型,而是等CPU发号施令,CPU告诉GPU说"你可以渲染这个模型了”,然后GPU才开始按照②中设置的Shader和贴图真正渲染这个模型,并把渲染后的结果层递到屏幕上。CPU告诉GPU“可以渲染这个模型”的过程或者说这个命令叫做Draw Call(我们在Stats面板上看到的Batches其实就是Draw Call的调用次数)。
从上面的流程可以看出,每一个3D模型要被渲染都应该会走完一个完整的步骤①②③。也就是说一个模型要被渲染,按理说就应该调用一次Draw Call。比方说我们场景中有3000个模型,那么Draw Call应该是3000,但是我们看Stats面板会发现Draw Call(Stats面板上的Batches值)并没有那么多。为什么会这样呢?因为Unity进行了批处理。
批处理就是把渲染时使用相同材质(Shader)、相同贴图的3D模型的网格合并在一起,成为一个大网格,然后再调用一次Draw Call,直接渲染这一个大网格。
所以需要注意的是,一定要使用相同材质和相同贴图的模型才可以批处理,一个模型使用的材质或贴图与其他模型不同,那么CPU就得单独进行步骤②设置渲染状态,紧接着也就得单独进行步骤③调用Draw Call。
从上面的分析可以看出,批处理的意义在于减少了Draw Call的调用。
因为CPU调用Draw Call之前,需要准备好数据,设置渲染状态,而准备数据和设置渲染状态特别耗时!如果Draw Call过多,那么CPU就会把大量的时间花在准备数据和设置渲染状态上,而造成性能问题。
举个例子,我们移动一个包含1024个1kb小文件的文件夹比移动一个1Mb的文件慢很多。因为计算机在移动文件的时候会有很多额外的操作,所以移动多个小文件比移动一个大文件更耗时。
那么渲染也可以这么理解,两个选项:
①CPU叫GPU渲染1000个小三角形
②CPU先把这1000个小三角形合并为一个大的网格,然后再叫GPU渲染这个大的网格
哪个更快?当然是②。因为①CPU要通知GPU1000次,而且每次都要花时间准备数据,设置渲染状态,而②CPU只需要通知GPU一次,也只需准备一次数据,设置一次渲染状态。即①的Draw Call是1000,②的Draw Call是1。
对于普通的3D模型,Unity内部做了静态批处理和动态批处理。
静态批处理和动态批处理的优缺点和限制可查看之前的博客,《Unity3D客户端项目优化总结之Stats统计面板》《Unity3D客户端项目优化总结之静态批处理Static Batching》。
从上面我们知道了一个3D模型是怎么被渲染出来的。那UGUI的渲染和3D模型的渲染有什么不同的吗?
答案是没有什么不同。
UGUI控件本质上也是网格,与3D模型不同的地方仅仅3D模型的网格是我们在3D Max或者Maya中建模建出来的,而UGUI控件的网格是控件代码代码里面去自动创建网格的。
比如我们创建一个Image和一个Text,将Scene视图的渲染选择为线框,可以看到其实Image和Text都是网格。
你可能会好奇Text的网格仅仅是一个矩形,怎么渲染出那么复杂的字呢?其实我们在Text上用的字体本质是个图集,渲染某个字就是把这个字对应图集上的图片渲染出来罢了,和普通的Image渲染本质其实没多大区别,区别在于有额外的模块去处理Text的字体图集与字对应的问题。
那既然UGUI的控件都是网格,那应该可以进行批处理吧?对的!对UGUI控件进行批处理就叫做UGUI的合批。
UGUI的合批就是把某个Canvas下满足合批规则的UI控件的网格合并为一个大的网格,然后将这些网格合并在一起,调用一次Draw Call,然后提交个GPU进行绘制。
那怎样才算满足合批规则呢?根据批处理的定义,只要两个网格使用的材质和贴图是一样的就可以进行批处理。
但是UGUI的合批还有其他规则,光满足材质和贴图相同还不行,具体是怎样的规则,我们后面会有一节专门讲这个事情。
那么问题来了,我们创建的默认Image和Text能否进行合批呢?
合批的基本条件是什么?材质(Shader)和贴图要相同,那我们来看看刚创建的默认Image和Text是否满足。
两者用的Shader都是默认的UI/Default。
那贴图是否一样呢?我们这里的Image没有指定贴图,直接在Inspector面板看不出来,得去Frame Debugger里面看看。
Frame Debugger的作用就是方便我们查看点击Enable时那一帧屏幕是如何一步一步绘制出来的,也就是说通过Frame Debugger我们可以知道先绘制了什么再绘制了什么最后绘制了什么。
Frame Debugger面板路径位于Window 》Analysis 》Frame Debugger。(我用的Unity版本是2019.3.15,低版本的Unity路径上可能会有所不同)
打开Frame Debugger的窗口。
然后点击Enable,Frame Debugger会展示出当前屏幕是怎么一步一步绘制出来的。
Frame Debugger的左侧是树状结构的,从上到下表示绘制内容的先后顺序,在上面的先绘制,下面的后绘制。树的根节点一般是Camera.Render,表示某个相机看到的画面是如何一步一步绘制出来的。
由于我们这里UGUI的Canvas的Render Mode选择的是Screen Space - Overlay模式,此模式是在所有相机绘制完成后再绘制UGUI的内容,所以在Camera.Render下还有个UGUI.Rendering.RenderOverlays。UGUI.Rendering.RenderOverlays表示UGUI是如何一步一一步绘制的。
选择某一项,Game视图就会展示当前选中项此时的画面。
我们点击Camera.Rendering下的Drawing,可以看到Game视图变成纯色,为什么会这样呢?我们按照树状结构依次看下去,发现其实最后执行了Clear (color+Z+stencil),Clear就是清除的意思,括号里面的内容是颜色缓冲区、深度缓冲区和模板缓冲区。也就是说Drawing这个步骤下面执行了清除颜色缓冲区、深度缓冲区、模板缓冲区。由于它清除了颜色缓冲区,所以整个画面就变成我们相机设置的颜色。
Clear完毕后,接着执行了Camera.ImageEffects,表示相机的屏幕后处理,就是相机看到的内容全部绘制完毕后,再把这相机的画面来进行处理,屏幕后处理可以实现一些特殊的效果。
我们创建一个相机,其默认是开启了HDR和MSAA效果的,所以这里会多一个Camera.ImageEffects步骤。(HDR和MSAA具体是什么,这里就不展开说了)
如果我们在Camera那里关闭HDR和MSAA,Camera.ImageEffects就不会再调用了。如果没有用到HDR和MSAA可以把它们都关闭了,这也是个优化的地方。
相机绘制完毕后,就接着绘制UGUI了(UGUI.Rendering.RenderOverlays)。
绘制UGUI的时候,首先进行了清除模板缓冲区,上面我们看到Camera.Rener已经清除了依次模板缓冲区,为什么UGUI这里又再一次清除呢?是因为UGUI的Mask(遮罩)控件会利用模板缓冲来实现遮罩的效果。(当然我们是不建议使用Mask来实现遮罩的,因为它至少会增加两个Draw Call,后面我们会讲到这个问题。)
清除模板缓冲后,就开始绘制我们的UGUI控件啦,还记得我们上面说的UGUI的本质是网格吗?所以绘制我们这里的Image和Text其实就是两个Draw Mesh(绘制网格)。渲染引擎才不管你是图片还是文字,在渲染引擎看来,所有UGUI控件通通都是网格。
我们点击第一个Draw Mesh,可以看到先绘制的是Image,右侧展示了绘制此Image使用的Shader及其贴图等。我们可以按住Ctrl然后点击_MainTex后的贴图框预览此Image使用的贴图。
然后我们点击第二个Draw Mesh,可以看到绘制的Text。但是为什么从Frame Debugger看到的字体贴图大小为0×0,说实话我也没整明白。
Text绘制完成后,我们的整个场景都绘制完毕了。
细心的同学可能会问,场景中,我们的Text不是在Image上面吗,是不是应该先绘制Text再绘制Image,Frame Debugger里面怎么是先绘制的Image呢?这个涉及到UGUI的合批规则了,先别急下面我们会专门说这个问题。
从Frame Debugger可以看出,Image和Text是分别绘制的,也就是说它们没有进行合批。原因也很简单,因为Image用的贴图时Unity White,而Text用的贴图是Font Texture。
Frame Debugger展示了我们看到的画面是怎样一步一步绘制出来,我们可以间接了解咱们制作的UI是否进行了合批。当然,除了通过Frame Debugger外,我们还可以通过Profier中UI模块更直观的了解咱们的UI是否进行了合批。
打开Profier的快捷键为Ctrl+7,菜单路径和Frame Debugger的路径一样,都是Windows》Analysis》Profiler。
具体操作如下。(ps:我用的Unity版本是Unity2019.3.15,Unity5的Profiler中好像没有UI这个模块)
下面我们来具体看看分析结果该怎么看。
我们这里主要看以下这几栏:Objcet、Batch Breaking Reason、GameObjects以及预览视图。
Object栏展示了批处理的顺序,每次合批都会有个编号,编号从小到大,编号越小的越先绘制。如Batch 0就比Batch 1先绘制,至于这个编号是怎么来的,等会儿说合批规则的时候咱们再讲。
Batch Breaking Reason展示了合批被打断的原因;GameObjects展示了每次合批合批的物体分别是哪些。从这里可看到,第一次合批(Batch 0)只有Image,第二次合批(Batch 1)只有Text,那为什么Image和Text为什么不能在同一个批次处理呢?看Batch Breaking Reason,可以知道原因是Differnt Textrure,就是说贴图不同导致了Image和Text不能合批,这与Frame Debugger的分析是一样的。
Frame Debugger与Profier UI模块的基本使用掌握了后,咱们就来看看UGUI合批的规则到底是什么。
我们先来直观的感受一下UGUI的合批。如图,在1的准备工作之上(新建一个新场景,然后删除灯光,设置相机的Clear Flags为Solid Color)新建3个默认的Image,其中Image和Imge (1)有重叠部分)。
然后我们看Stats面板,Batches值为2(相机的MSAA没关闭的话会自带一个Batches),说明3个Image只调用了一次Draw Call,即这3个Image进行了合批。
然后我们再看看Profier-UI,可以看到,3个Image进行了合批,一次性把3个Image给绘制出来了。这就是UGUI的合批。
然后,我们在Image和Image (1)之间创建一个Text,该Text与Image (1)有重叠部分。
此时我们再看Stats面板,会发现Batches值变成了4。
我们上面知道,因为Text控件和Image控件的渲染时使用的贴图不同,所以两者不能合批。但是在上面这个场景中,Image、Image (1)和Image (2)是能合批的,另外加了个Text,Batches值也应该为3才对(相机自带的一个+3个Image的+一个Text的),但Stats面板为啥显示为4呢,比我们分析的多了一个?
然后我们去看看Profier-UI。
可以看到,Image、Image (1)、Image (2)三者并没有合批,只有Image、Image (1)合批了,Image (2)是单独的一个批次。
也就是说,原本Image、Image (1)、Image (2)三者原本能够合批,但是由于Image (1)下多了个Text,就导致Image (1)不能和Image、Image (2)合批了。
换句话说就是,Text将Image、Image (1)、Image (2)三者的合批给打断了!
那这个Text为什么会打断它们的合批,以及我们该怎么去解决合批被打断的问题呢?
要回答这两个问题,我们就得先弄清楚UGUI的合批规则了。
两个UI控件能合批的基本条件是这两个控件使用的材质球(Shader)和贴图要完全相同。比如上面看到的,虽然Text和Image默认使用的材质球都是UI/Default,但是两者使用的贴图不同,所以注定Text和Image无法合批。材质和贴图相同这只是基本条件,还有其他规则。UGUI中完整的合批流程(规则)如下。
首先我们要明确UGUI中Canvas下可以嵌套子Canvas,但是合批是以Canvas(不包含子Canvas)为单位的(子Canvas会是另外一个批次了)。除此之外,合批的操作是在子线程完成的。
①既然合批是以Canvas为单位,第一步自然就是把所有Canvas给找出来,然后剔除掉不必渲染的Canvas(透明度为0,长宽为0,在RectMask2D控件下,且在RectMask2D的区域外)
②然后计算Canvas下各UI控件的深度值Depth(需要注意的是Image的属性里面也有个depth,两者不是同一个东西)
③Depth的计算规则如下:
⑤得到VisiableList之后,判断VisiableList中相邻的元素是否能够合批(相同的材质和贴图)。需要注意这里不再考虑Depth是否相同,只要两个元素相邻然后材质和贴图相同,即使两个元素的Depth不相同,这两个元素也能合批。然后一个批次一个批次的合并网格,提交GPU进行渲染。
除此之外,需要注意的是,合批是将同一Canvas下多个UI的网格合并在一起,如果其中任何一个元素的材质、网格顶点、位置(Transform)甚至颜色或者在该Canvas下动态创建或删除UI元素都将导致该Canvas重新计算合批(需要注意的是仅仅会影响这一个Canvas,子Canvas或父Canvas以及其他Canvas不会重新计算),重新生成新的网格,这个重新计算生成网格的过程被称为rebuild。所以,这也是为什么做UI提倡动静分离(动态部分和静态部分分别用不同的Canvas),层级尽量减少(层级多了,重新计算更耗时)的原因。
合批的规则搞清楚了,但彻底弄懂还需要练习一下。我这里专门挑选了几个例子,跟着做一遍应该能大大加深理解了。
在开始之前,我们得先知道material Id和texture Id怎么获取到,其实很简单,直接GetInstanceID()
就行了。
// materialId
image.material.GetInstanceID()
// textureId
image.mainTexture.GetInstanceID()
如图,三张Image使用的材质都是UI/Default,Image1和Imge3使用的贴图的texture Id = 13188,Image2的texture Id = -1136。
现在,请分析它们三者的合批情况和渲染顺序(先渲染哪张图,再渲染哪张图)?
①首先,先分别计算三张图片的Depth(还记得Depth是怎么计算的吗?忘了再去上面看看)。
UI | 合批的Depth |
---|---|
Image1 | 0 |
Image2 | 0 |
Image3 | 1 |
②然后按照依次Depth、material Id、texture Id、hierarchy sort order进行升序排序,得到VisiableList。
③最后判断相邻元素是否能否合批,计算合批次数。
Image2和Image1材质相同但贴图不同,所以Image2和Image1不能合批;
Image1和Image3材质和贴图均相同,所以Image1和Image3可以合批(这里需要注意的是,虽然Image1和Image的Depth不相同,但是这里合批是不再考虑这个问题);
也就是说Image2单独绘制,Image1和Image3合批再绘制一次。
我们去Profiler UI看看咱们的分析是否正确。
可以看到咱们的分析是正确的。
如图,三张Image使用的材质都是UI/Default,Image1和Imge3使用的贴图的texture Id = -1136,Image2的texture Id = 13188。
现在,请分析它们三者的合批情况和渲染顺序(先渲染哪张图,再渲染哪张图)?
①首先,先分别计算三张图片的Depth。
UI | 合批的Depth |
---|---|
Image1 | 0 |
Image2 | 0 |
Image3 | 1 |
②然后按照依次Depth、material Id、texture Id、hierarchy sort order进行升序排序,得到VisiableList。
③最后判断相邻元素是否能否合批,计算合批次数。
Image1和Image2材质相同但贴图不同,所以Image1和Image2不能合批;
Image2和Image3材质相同但贴图不同,所以Image1和Image3也不能合批;
也就是说Image1、Image2、Image3都是单独绘制,共三个批次,绘制顺序为Image1、Image2、Image3。
我们去Profiler UI看看咱们的分析是否正确。
可以看到咱们的分析是正确的。
知道合批的原理之后,咱们就知道UI如何优化了。
当然,上面这些只是一部分。
还有,尽量不要使用Outline、Tiled Sprite(这两者会多生成许多顶点),不需要响应点击事件的取消勾选Raycast等等,但是这些和UGUI合批的关系不大了,至于它们为什么可以优化,得从UGUI的源码入手了,有空我们再来说说吧。
最后,附上测试项目。
链接:https://pan.baidu.com/s/1Git6Qhr0Y8Lef8z7dtwddg
提取码:xfk3
博主本文博客链接。
ps:前面3个文章内容其实有点问题的,大家可以和这篇文章对比一下然后实验一下看哪个是正确的。欢迎批评指正。