UGUI DrawCall合批细节(一)——合批的规则
UGUI DrawCall合批细节(二)——Mask影响合批
UGUI DrawCall合批细节(三)——RectMask2D与Mask的区别及选择
UGUI避坑指南
转发大佬的文章,讲得比较清楚了,也方便自己学习。
一、合批的规则
UGUI在合批之前,会根据ui的Depth、MatID 、ImgID、RendererOrder进行排序,之后对相邻的UI进行检测,判断ImgID和MatID是否相同,如果相同则可以进行合批处理,如果这两个UI的MatID和ImgID都相同,但是不连续,中间有其他不同MatID或ImgID的UI则会打断合批。
Depht排序:
1.先筛选掉Depht为-1的值,这部分默认不渲染;
2.接着判断是否该元素底部是否有物体,如果没有则负责Depth为0,如果盖住物体(这块是通过Mesh进行判断,判断Mesh是否相交)则等于底部盖住的UI元素中Depth最大的值+1;
3.如果两个相邻元素通过了合批测试,则这两个相邻元素的深度值相等;
4.深度排序之后,就会根据matID进行排序,如果材质相同则对ImgID进行排序,如果也相同,那会根据inspection面板上的RendererOrder,最后真正进行UI的合批。
如图示:
hierrchy面板顺序如图
drawCall如下:
可以看到是5个,因为文本打断了Depth的合批。
在Debug Frame里我们可以清楚的看到他有3个DrawMesh
那只需要将hierrchy的顺序修改到最后,我们看会发生什么。
hierrchy面板顺序如图:
drawCall如下:
降为了4,代表三张image进行了合批。
用FrameDebug也可以清楚的看到优化是成功的。
特殊情况:
下面我们在看一个特殊的案例
请注意面板上的顺序,按我们理解的情况看,他应该排序后的顺序是:文本,图片,图片,之后因为两个图片的材质一致所以能够进行合批。
但是通过frameDebug我们发现,实际上顺序却是:图片,文本,图片这样的顺序,那这是为什么呢,我们更进一步,通过Profiler的UI的modules来看具体的细节
给出的原因是贴图不同,目前两个图片的贴图都是Nil,那我们尝试修改两张图片的贴图
之后我们再观察会发现
这里的顺序就是我们预期的顺序文本,图片,图片了,那为什么会发现这种问题呢,是因为在默认情况下,排序了depth之后,这两个层级相同,就会比较matID,image和text的默认材质都是ui defult,mat也一致,在比较TextureID时,img为nil比默认的Text的TextureID小,所以排序在了text前,但是替换了贴图后,图片的TextureID大于Text的TextureID,下面的显示就达到了预期的效果。
二、Mask影响合批
在UGUI的开发中我们常谈少用Mask,但是为什么少用,用了Mask又会影响什么呢,今天我们就来简单说说。
(一)被mask的元素为什么不能和外部元素合批
首先我们先来聊聊mask的实现,我们简单搭建一个测试场景
会发现多了一个mask会多出3个drawCall,我们查看Mask的源码
他在StencilMaterial.Add的时候为这个maskUI增加了一个新的材质,导致了mask内的物体无法和外部同样材质的物体合批,这是其一,其二是mask会进行两次pass,第一步是对在模板缓冲中的值进行赋值,将要显示的部分缓存值设置为1,不显示的部分设置为0,在第二个pass绘制时对模板缓冲值为0的部分进行剔除,所以这两个pass也会带来两个drawcall的生成。所以添加一个mask最起码会增加3个drawcall。并且了解原理我们会发现,其实虽然是不显示但是被mask剔除的部分还是存在的,只是不会进行绘制而已,这也会影响到我们后面会说的,层叠下异常增长的drawcall,这个我们后面来详解。
(二) 不同mask之间的合批
不同的mask之间是可以合批的,在我们上一章《UGUI DrawCall合批细节(一)》中讲解过,相同材质和贴图的两个UI物件是能够进行合批的,虽然Mask会对原来的材质进行替换,添加一个新的材质,但是新添加的材质和新添加的材质之间就支持合批,所以我们现在简单添加两个mask查看drawCall效果:
一个Mask的情况下:
两个Mask的情况下:
我们会发现drawCall数量并没有增加,证明了我们的推论是正确的,但是这里有一个例外情况,也是我们接下来要论述的第三点也是最容易被人忽略的点。
(三)被mask的物体只是不被绘制,依旧会影响合批计算
我们先看我现在mask的物体被剔除的部分:
可以发现在下部分的红色方块被剔除了,那用第二节的理论验证了mask之间会进行合批的,但是是绝对的吗,并不是,我们看一个情况。
同样的物体我们把他移动到上一个mask的最底部我们发现合批并没有出现,而是又增长了3个drawCall,这是为什么?
细节就在这里被遮罩剔除的区域,实际上也是会参与合批计算的,当这两个mask不重叠的时候,他们depth相同,mat相同,可以进行合批,但是当他们重叠时,因为被剔除的部分参与了运算,发现depth不同并且mat也不同,就无法进行合批,会增加3个Drawcall,这就是很多时候项目里明明没重叠但是却多了很多drawCall的情况。
三、RectMask2D与Mask的区别及选择
RectMask2D
那我们同比分析一下UI上常用的第二个Mask组件RectMask2D,等同于Mask的测试场景,我们测试单个mask2D开启和关闭对drawCall的影响。
开启前:
开启后:
我们可以发现DrawCall只增加了一个,因为内部的元素无法和外部进行合批增加了一个drawCall,并且内部的元素自己能够进行合批。而且和Mask不同的是,mask2D并不会产生两额外的pass增加两个drawcall的消耗。这是为什么呢?
查看rectMask2D的源码我们可以发现,他并没有替换材质的过程,没有用到模板缓冲的实现方式,而是通过了canvasRender里面进行了ClipRect的剔除,这样相比于Mask会带来以下不同:
(一)RectMask2D之间无法进行合批
我们复制一份mask2D对象可以发现,drawCall增加了1
查看差异的理由可以发现是因为不同的裁剪区域,所以我们能知道,RectMask2D之间内部的元素是无法进行合批的,这也是和Mask不同的部分
(二)被mask隐藏的物体不会参与合批计算
相比于mask不同,我们这里添加了RectMsk2D,虽然进行了重叠但是我们发现draw并没有增加,并且做一个测试,将同一个mask下的材质换成不同的贴图后,将一个mask移出区域,
会发现根本不会计算移除出区域的图片的drawCall,所以这是和Mask的第二个区别。
(三)RectMask2DUI组件上挂载的Image可以参与外部的合批
我们做一个简单的测试:
可以发现在添加和移除Image组件时,drawcall的数量没有增加,因为新添加的img和外部的黄色方块的材质一致,进行了合批。而Mask组件没有Image组件的话都无法生效,这是和mask的第三个区别。
总结:
所以到底我们项目是使用Mask还是使用RectMask2D呢?在我看来这是应用场景的选择问题,当界面只有一个mask需要使用的时候,RectMask2D无疑是最优解,只会带来一个drawCall的增加,如果是多个Mask并且互相是可以合批的,那无疑Mask更适合在那种情景下使用,但是使用的时候也需要注意被剔除区域的层叠问题。
四、UGUI避坑指南
想到啥写啥吧,就随笔类似记录知识点一样一个个说,有空的话再编辑或者再开新帖具体讲讲某些点,都是UGUI优化的建议:
(1)优化填充率,裁减掉无用的区域,镂空等;
(2)Mask的使用以及选择,或者用自用Mask,这块原理我在UGUI的合批里讲过,可以翻一翻我前面博客这里不再复述;
(3)少用unity自带的outline和shadow,会大量增加顶点和面数,比如outline,他实现原理是复制了四份文本然后做不同角度的便宜,模拟描边,要不就用自己实现的(挖坑待填);
(4)操作全屏UI时建议将场景相机移走或者关闭,降低渲染面数,因为就算是被全屏UI遮挡住了,实际上后面的场景还是被渲染的占用资源;
(5)如果非特殊需求没必要使用CanvasPixedPerfect,因为比如在scrollView时,滑动视图时一直会导致不断重绘产生性能损耗;
(6)不需要接受点击时间的物件将RayCastTarget关掉减少事件响应,从底层看是因为UGUI的RayCastTarget的响应是从数组中遍历检测是否和用户的点击区域响应,所以能够缩减数组大小自然能够缩减遍历次数;
(7)UI动静分离,因为如果了解过Unity的UGUI源码就能发现Batch building 的过程,Canvas 会将其 ui 元素生成的 mesh 组合并生成合适的绘制命令给 Unity 渲染系统。并且过程的结果会被缓存并重用,直到 Canvas 重新被标记为脏。这会在组合的网格发生变化时发生。Canvas 会根据子节点里带有 Canvas Renderer 组件的 ui 来生成 网格,但是不包括 Sub-Canvas 的子节点,也就是说每个 Canvas 单独负责自身的 Batch building。Batch building 的过程会对根据深度、重叠测试、材质等对各个 Mesh 进行排序、分组、合并,这个过程是多线程的,在移动端(核心少)与桌面端(核心多)会呈现相当大的差异。Canvas下的某个元素进行变化时都会导致同一个Canvas下的所有元素都进行网格重建,这样会导致某些静态部分的网格被不同重绘导致额外性能损耗。而常用的拆分canvas有两种,一种是在同一个根节点下new一个动态Canvas,类似图示:
这种用法的缺点是层级关系不好调控,当静态Canvas下的某些物体需要显示在动态canvas下的物件上时就很麻烦。
所以我推荐第二种方式子Canvas的方式来实现这点。
类似上图在子物体上挂在subCanvas,这样的好处是它与其父节点是隔离的,Sub-Canvas 的 rebuild 不会逼迫父节点重建几何,反之亦然,不过还是存在一些边界情况。比如父节点变化导致子节点大小变化。但是Canvas会打断合批,所以比如只是拖拽某个组件的操作,可以在开始拖动时挂载subcanvas,结束拖动移除canvas,这样的做法能够保证物体的合批顺序,同时兼顾了动静分离。
(7)对于UI上常用的改变颜色的操作,不要使用Image组件上的Color属性改变颜色,这样会导致整个Canvas的重建,可以新建个材质,设置材质给Image的Material通过修改材质的颜色来达到同样效果
(8)对于界面上常用的组件隐藏,别使用SetActive来控制显影,有两种方式,一种是通过设置物体上的CullTransparentMesh并且控制物体透明度进行剔除,这个一般用于单个ui,如果是多个UI要不显示的话通过设置canvasGroups的Alpha控制显影。
(9)Text的组件的BestFit非必要别开,因为这样开启这个会不断生成各种尺寸的字号字体图集,增加不必要开销。