UGUI优化
前言
从Unity4.6版本加入UGUI以来,使用案例越多越多,相比NGUI,UGUI的效率要高的多
- Ugui的网格合并实在C++代码中执行的,Ngui是在C#中执行
- Ugui网格合并是多线程,Ngui是在主线程
- Ngui维护图片的Depth比较困难,而Ugui渲染顺序是依据Hierarchy中的顺序
- 重点是 Unity堆出众多的UI视图工具都是针对UGUI的,例如Frame Debugger,Profiler中的UI模块
虽然不久之后应该会被其他的UI方案替代,但是现阶段最实用,最流行的UI方案还是Ugui.UI使用不当会造成各种问题,现在对Ugui进行下优化总结.
UGUI官方源码地址: https://bitbucket.org/Unity-Technologies/ui/src/
针对Ugui优化的四个方向
- 过多的GPU片段着色器使用率(如屏幕填充率过高)
- 过多的CPU时间开销在重建一个画布上 (画布划分)
- 过多的画布网格重建次数
- 过多的CPU时间开销在生成顶点上(通常是文本)
网格重建
UI中大多数的问题都是网格重建引起的
2020年4月19更新
放知乎一个看到了一个帖子:https://zhuanlan.zhihu.com/p/128546981
什么是网格重建
UGUI中以Canvas为单位,其下的UI原色会合成一个大Mesh,当我们改变UI元素(改变颜色,图片,大小等等)的时候都会引起网格Rebuild或ReBatch(UI重新批处理),每当有一个UI元素变化就会影响当前Canvas所有的UI元素引发网格重建.当UI过多的时候会造成重建时间过长 造成卡顿.
简而言之,就是Canvas会合成一个大的mesh,其地下的UI元素更改,会导致整个大mesh引发更改.
哪些操作会引发网格重建?
- 修改Image. rawImage中的贴图
- 更改RectTransform属性(会引发
OnRectTransformDimensionsChange
回调) 改变Position,Rotation,Scale不会调用 - UI的Enable/DisEnable
- Text的文字内容替换
- 更改UI顶点颜色(一般的color属性)
附上一个可以查找引发网格重建元素的Script,可以查看那些执行了Rebuild,无法查看Rebatch(ReBatch在底层c++代码中) (来自雨松momo 链接)
参考:UGUI笔记——UI Mesh Rebuild - 简书 (jianshu.com)
关于网格重建的一些Tips
网格重建可能我们无法避免,但是我们可以通过一些操作来减少网格重建,网格重建包括ReBuild和ReBatch两部分,针对这两部分我们可以进行一些优化,值得一提的是Unity5.2以后批处理已经重写,运用到了多线程,这部分的消耗时间大大减少,具体可以参考这篇文章: https://blog.csdn.net/cyf649669121/article/details/83142903
在UI源码中重建的标志就是设置了这个UI为dirty
,例如SetVerticesDirty
SetLayoutDirty
SetMaterialDirty
SetAllDirty
等等
避免使用UI的
Active/DisActive
,因为基类Graphic 的在调用OnEable的时候 会执行 SetAllDirty 导致网格重建.对于整体Canvas的显隐:最好就是canvas的render mode是
Screen Space-Camera
,增加一个layer ,camera剔除这个layer渲染,控制显隐的时候就通过控制canvas的layer对于整个个plane:可以添加
Canvas-Group
控制alpha来显隐对于单个UI :可以通过控制
scale=0
,或者在Canvas
添加Mask2DRect
,然后将单个UI移除Canvas之外.这两个操作应该都会Rebatch(因为引发了Positon和scale),发现一个比较好的就是设置CanvasRenderer
的alpha(设置CanvasRenderer的alpha=0的时候同同时还要设置UI的raycast target=fase,不然还会相应按钮点击)另外知乎上一篇文章可以利用
CanvasRenderer
cull属性,即:image.canvasRenderer.cull = true
https://zhuanlan.zhihu.com/p/30492759再另外可以研究一下CanvasRenderer
EnableRectClipping
这个方法
避免使用layout组件
修改UI元素可以修改材质TintColor
- 修改的是Image组件上的Color属性,其原理是修改顶点色,因此是会引起网格的Rebuild的(即Canvas.BuildBatch操作,同时也会有Canvas.SendWillRenderCanvases的开销)。而通过修改顶点色来实现UI元素变色的好处在于,修改顶点色可以保证其材质不变,因此不会产生额外的Draw Call
- 在UI的默认Shader中存在一个Tint Color的变量,正常情况下,该值为常数(1,1,1),且并不会被修改。如果是用脚本访问Image的Material,并修改其上的Tint Color属性时,对UI元素产生的网格信息并没有影响,因此就不会引起网格的Rebuild。但这样做因为修改了材质,所以会增加一个Draw Call
注意点:
在UI的默认Shader中存在一个Tint Color的变量,正常情况下,该值为常数(1,1,1),且并不会被修改。如果是用脚本访问Image的Material,并修改其上的Tint Color属性时,对UI元素产生的网格信息并没有影响,因此就不会引起网格的Rebuild。但这样做因为修改了材质,所以会增加一个Draw Call。 这个没发现增加DC,但是Setpass Call 会增加一个,内存中材质也会增加几k,可以考虑使用MaterialPropertyBlock类
1
2
3
4
5
6
7
8
9void Start()
{
image = GetComponent<Image>();
image.material = Instantiate(image.material) as Material;
}
private void ChangeColor()
{
image.material.SetColor("_Color", color);
}Canvas动静分离,就是静态UI元素可以存放到单独的canvas,虽然多个canvas会打断批处理,但是对对于变化过多的UI这还是很有用的.静态canvas里面的UI 不会网格重建
谨慎使用Canvas的Pixel Perfect选项:该选项的开启会导致UI元素在发生位移时,其长宽会被进行微调(为了对齐像素),从而造成layout Rebuild。(比如ScrollRect滚动时,会使得Canvas.SendWillRenderCanvas消耗较高)
持续更新….
UI的批处理
UI好的批处理可以明显降低DC,有利于界面流畅的切换,我们可以使用工具Frame Debug 来查看,也可以在proliler的UI模块中查看.
UGUI批处理原则
UI渲染是从后向前渲染的,也就是在Hierarchy视图中从上往下
合并原则:同一个深度,同一个贴图,同一个材质
计算方法: 从直观的角度来解释计算层级号的算法,如果有一个UI元素,它所占的屏幕范围内(通常是矩形),如果没有任何UI在它的底下,那么它的层级号就是0(最底下);如果有一个UI在其底下且该UI可以和它Batch,那它的层级号与底下的UI层级一样;如果有一个UI在其底下但是无法与它Batch,那它的层级号为底下的UI的层级+1;如果有多个UI都在其下面,那么按前两种方式遍历计算所有的层级号,其中最大的那个作为自己的层级号。
有了层级号之后,就要合并批次了,此时,Unity会将每一层的所有元素进行一个排序(按照材质、纹理等信息),合并掉可以Batch的元素成为一个批次,目前已知的排序规则是,Text组件会排在Image组件之前渲染,而同一类组件的情况下排序规则未知(好像并没什么规则)。经过以上排序,就可以得到一个有序的批次序列了。这时,Unity会再做一个优化,即如果相邻间的两个批次正好可以Batch的话就会进行Batch .
有了层级号之后,就要合并批次了,此时,Unity会将每一层的所有元素进行一个排序(按照材质、纹理等信息),合并掉可以Batch的元素成为一个批次,目前已知的排序规则是,Text组件会排在Image组件之前渲染,而同一类组件的情况下排序规则未知(好像并没什么规则)。经过以上排序,就可以得到一个有序的批次序列了。这时,Unity会再做一个优化,即如果相邻间的两个批次正好可以Batch的话就会进行Batch .
可以合并这里先看看示例1:
先看button底下没用别的UI 那么他的深度(层级)就是0,然后看button1和button一样,深度=0,然后看text,text下面是button的img图片,由于text贴图和button贴图不同,所以不能合批
,深度为1,两个text都是1。算好层级之后,将所以的UI放入渲染队列,两个text合批成一个dc,两个buton img合批成一个dc,一共两个
很奇快同样的两个UI元素却又不同的DC,这里面就是UI的批处理原因
先看button,底下无,深度=0,再看text,text和button img贴图不同
不能合批,所以text1=1
然后button1,底下有个text1,text1=1,button1和text不能合批,所以button1=2,最后button1下面的text2和button1也不能合批,text2=3,所以都不能合批一共四个dc
再来看看另一种
这种情况只有两个dc
直观上看,button和button1虽然有重叠,但是他们的材质纹理都相同,所以他们可以合并,然后两个text合并,所以只有两个dc
再看个例子:UI批处理只看网格重叠
计算过程:先看绿图下面有红图材质贴图都一样可以合并,红图下面有蓝图也可以合并,三个归为一个层级,我们记为0,这时候看text,虽然text的矩形框有覆盖关系,但是网格并没有,UI批处理只看网格重叠
所以这里Dc=2
然后我们改变一下:
这个text就打断了批处理,所以蓝图无法和红绿图合批,所以蓝图一个dc,text一个dc,红绿一个dc一共三个DC
我们也可以在Profiler中查看原因
一个特殊的例子
如果image=null,那么他的渲染顺序是大于text的,所以这就导致了一些问题,如下
在实际渲染队列中,blue=0,text=1,green=2,贴图都是null,首先渲染blue=0,然后是green,可是green下面有个text,所以比text大1,text无法和blue合批所以是1,那么green=2
但是我们想得到的是text单独渲染,green和blue合批。那么这时候只要给两个图片指认相同的贴图即可
由于image有贴图所以先渲染text=0,然后是green=1,blue应该也是0但是无法和text合批,所以+1,这时候blue和green深度都为1且其他都相同,所以可以合批,但是这给我们一个提示:图片组件最好不要保持为null
Mask组件
mask组件会打断合批,原因是材质不同
mask使用的是模板缓存,还会增加2个dc(自身一个,依赖image组件又一个)
mask之间有重叠会增加dc
mask内部图片可以合批,但无法和外面图片合批
这个很好理解,内部图片与外部图片材质不同,无法合批.但是内部图片都是用的用同一个材质,他们可以合批
…这里有个奇怪的现象,一开始运行发现有四个dc,然后把两个image隐藏又显示发现又只有3个dc了😪😪😪
可能是另外两个mask没有裁剪什么吧,然后我让另外两个mask剪裁,发现可以了,很疑惑🤐🤐🤐🤐
- 重叠mask看不到的地方也会影响到深度计算,从而打断合批
RectMask2D组件
RectMask2D组件相对于mask组件的好处
- mask2d不会增加dc
- mask2d不会影响深度计算,即mask2d看不到的地方不会参与计算,网格顶点都不会计算
但是mask2d也有不足
- mask2d与mask2d内部无法合批
如上图 每一个mask2d里面的image都会是一个dc
但是mask2d里面UI元素是可以合批的
mask2d和mask2d上面的元素是可以合并的
原本是4个dc,增加了一个image,变成了5个dc,虽然mask2d上面的UI元素和内部UI元素材质贴图都一致,但是因为mask2d的关系,无法导致合批。
一些特殊的打断
- UI的pos.Z不为0,会打断合批
- Canvas与Canvas不会合批,但是UI的动静分离也还是有必要的
- 一些旋转也会打断合批,旋转Z不会打断合批(比较怪的现象)
但是!!!!
很奇快了…
填充率
Fill Rate(填充率)是指显卡每帧每秒能够渲染的像素数(通俗点说就是图片越大需要绘制的像素越多). 在每帧绘制中,如果一个像素被反复绘制的次数越多,那么它占用的资源也必然更多
简单来说就是overdraw,也就是我们要减少UI占用的像素,防止同一个像素被太多次绘制
中间突出的一部分就是被多次绘制
一些减少填充率的操作
当需要用一个透明图片充当遮罩的时候,可以把这个设计一个
空图片
1
2
3
4
5
6
7
8using UnityEngine.UI;
public class Empty4Raycast : Graphic
{
protected override void OnPopulateMesh(VertexHelper toFill)
{
toFill.Clear();
}
}这时候看屏幕并没有绘制
对于某些图片不影响外观,可以设置image的fill center=false
对于某些不规则图片
不规则图片也是占据整个矩形
打开Sprite Editor
但是这样增加了顶点,
没有绝对的优化,只能根据你的项目需求来进行相关的优化
UI的点击性能优化
更新于2020年4月19日21:32 照搬于知乎
链接:https://zhuanlan.zhihu.com/p/55566751
文章内容(csdn文章)说的很多,先慢慢一个个分析吧
- Unity中即使取消了RaycastTarget,还是会会进入raycast列表的计算。我们一般知道的优化方案是如果UI组件不交互就取消了RaycastTarget,但是还是会计算 然后排除,所以这里如果计算量大的话需要注意。
- 然后就是一些官方脚本内部的问题 可以尝试按照csdn博客文章修改
其他的一些Tips
- 少使用或者避免使用layout group组件
- 最好不要使用UI自带的特效(outline,shadow…)额外增加dc 顶点 ,Git上面有个特效UIEffect
- Image为null或者alpha为0不能降低开销,Image为null还会会打断合批
- Canvas设置摄像机模式的时候一定要指认camara, Camera.main实际是调用了FindObjectWithTag 很费
- UI 图片旋转之后出现锯齿 解决办法:Canvas 模式设置成ScreenSpace-Camera模式
- 关闭不必要的
RaycastTarget
,如果UI不需要交互 就关闭,这里附上一个用于检测是否关闭RaycastTarget 脚本,知乎上面的一个优化射线检测方案https://zhuanlan.zhihu.com/p/55566751
1 |
|
写在最后的话
Ugui也用了很久,之前不知道UI会有这么多的坑,鉴于坑太多,Unity官方也不填坑,直接搞了一个UIwidgets和UIElements出来,不出意外未来UIelements会替代UGUI
UIElemetns出来官方肯定会宣传,啊多么多么好,多么流畅,不用开发者担心xx问题,🤪
参考链接:
- UWA:UI模块优化直播视频:https://v.qq.com/x/page/e0520qxtjox.html
- 知乎上一个(其实对上面做了简单的总结):https://zhuanlan.zhihu.com/p/43111806
- 《小米超神》技术总监王啸予:重度MOBA的优化之路 https://zhuanlan.zhihu.com/p/38004837