本系列是学习siki学院UGUI整体解决方案-优化篇(Unity 2019.1.0f2)笔记
github地址:https://github.com/BlueMonk1107/UGUISolution
图集分块算法地址:https://github.com/DaVikingCode/UnityRuntimeSpriteSheetsGenerator
texturepacker官网:https://www.codeandweb.com/texturepacker
UGUI源码下载地址 :https://bitbucket.org/Unity-Technologies/ui/downloads/?tab=downloads
透明物体的渲染是不会写深度的,渲染半透明的物体从后向前渲染,会引发over draw的问题,这也是为什么耗性能的原因
UI都是在透明队列中绘制的,因此UI中的所有东西都带有二八混合,全都是从后向前绘制,因此UI部分需要优化over draw,有大量的UI进行绘制时会超出移动设备CPU渲染能力
深度值 :深度越大,离摄像机越远
深度测试小案例 :使用同一个shader的物体,开启ZWrite后会显示在最前面
Shader "My Shader/AlphaShadera"
{
SubShader{
//开启深度测试
ZWrite on
ZTest Always
Pass{
Color(1,1,1,1)
}
}
}
绝大部分的UI问题归根结底是网格重建问题引发的
是将canvas下所有物体合批进行统一处理,进行mesh的生成。
canvas下任何一个物体发生了改变,就会引起合批的操作,将canvas下所有物体进行重新绘制,因此性能消耗大
有的项目UI在同一个canvas下,那么canvas下的网格全都会重新绘制
针对单个物体的重绘,在canvasbuildbatch会计算有哪些元素需要进行Rebuild
之前在CanvasUpdateRegistry的PerformUpdate方法中提及主要进行了三部分
涉及到很多的计算,比如嵌套的layout会引起更大的性能损耗,因为他是一层层计算下去,只有计算好上一层才能进行下一层的计算,特别只有两三项的时候,最好使用代码进行计算避免使用自动排序组件
对顶点和材质生成脏标记
材质生成脏标记的原因很单一,只有当材质进行改变的时候才会进行在那个标记
顶点脏标记的原因就很多了,在Graphic类中SetVerticesDirty(设置顶点在那个标记的方法),查看其引用,可以看到引用的地方有很多,比如image中一个小属性的改变就会引起顶点脏标记从而引起重构
在Graphic的OnEnable和OnDisable中会调用SetAllDirty,他会将所有的脏标记全部打开
直接控制组件以及物体显隐的性能损耗较大就是因为调用了这个方法,因此在组件以及物体显隐时都会进行重绘,要避免调用物体的SetActive方法
以Image设置颜色举例
// Graphic类中color属性,设置时会先调用SetPropertyUtility.SetColor,成功之后设置脏标记
public virtual Color color {
get { return m_Color; }
set {
if (SetPropertyUtility.SetColor(ref m_Color, value))
SetVerticesDirty();
}
}
// SetPropertyUtility.SetColor : 颜色修改
public static bool SetColor(ref Color currentValue, Color newValue)
{
//判断与之前的颜色是都一致,一致则不需要重建
if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a)
return false;
currentValue = newValue;
return true;
}
//SetVerticesDirty : 调用CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild
//传入需要重建的Graphic
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
//CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild : 将传入的Graphic加入队列中
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
if (m_PerformingGraphicUpdate)
{
Debug.LogError(string.Format("Trying to add {0} for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.", element));
return false;
}
return m_GraphicRebuildQueue.AddUnique(element);
}
//CanvasUpdateRegistry构造函数注册了Canvas.willRenderCanvases += PerformUpdate
//下一帧canvasrender时会调用PerformUpdate就会进行加入的Graphic重建
总结来说 :
就是物体的修改导致被标记上脏标记,然后对标记上在那个标记的物体Graphic进行重建
canvas的重建是最耗时的,底下有一大堆东西,因此修改了其中一个组件就会将其上的所有组件进行重建,如果说其他组件非常多但是并不会被修改的,比如背景,但是由于修改了其他组件导致并不需要经常修改的组件的改动,这样就造成无端的性能消耗
或者画面上有很多的text,那么控制其中一个text的显隐就会引起所有text的重建,会引起卡顿,因为text上有很多面数和顶点数
主要组件是GraphicRaycaster(Canvas上)组件
与普通射线相比最大区别只会对继承了Graphic类的对象产生响应,作为UI射线处理
//主要是Raycast方法 : 代码太多,只截取部分代码
//获取当前canvas所有Graphics组件
var canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
//响应点击事件的摄像机
var currentEventCamera = eventCamera;
//获取canvas上设置的targetDisplay : 显示在第几个显示器上
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
displayIndex = canvas.targetDisplay;
else
displayIndex = currentEventCamera.targetDisplay;
//将点击位置转换成相对于显示器屏幕的坐标(计算Pos属性)
//判断点击Pos转换到屏幕坐标的结果是否超出了屏幕
if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
return;
//根据GraphicRaycaster上的Blocking Mask属性判断射线击中的距离(计算hitDistance属性)
//射线的主要逻辑 : 对当前Graphics的状态进行筛选
Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
//graphic深度为-1时不会被绘制在屏幕上,即物体的activeInHierarchy为false
//graphic.raycastTarget : UI元素是否进行射线碰撞响应
//graphic.canvasRenderer.cull : 当前canvasrender是否被剔除,如果为true也不会绘制
if (graphic.depth == -1 || !graphic.raycastTarget || graphic.canvasRenderer.cull)
continue;
//筛选点击时间是否在graphic的rectTransform上
if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
continue;
//筛选Z的租表是否超出了相机的最远面
if (eventCamera != null && eventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > eventCamera.farClipPlane)
continue;
//根据深度对数组进行逆序
s_SortedGraphics.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
totalCount = s_SortedGraphics.Count;
for (int i = 0; i < totalCount; ++i)
results.Add(s_SortedGraphics[i]);
//判断当前是否是一个反面目标
if (ignoreReversedGraphics)
//过滤摄像机背后渲染的元素
if (appendGraphic)
//再判断距离
if (distance >= hitDistance)
continue;
//得到真正的击中目标
resultAppendList.Add(castResult);
合批是指能够合并mesh的UI合在一起,UI是从后向前构建的,面板上的顺序就是渲染层级
首先根据UI Depth、Material ID、Texture ID、Render Order对所有的UI进行排序,然后进行合批处理,即判断Material和Texture是否是一样的,一样则认为可以进行合批
这里注意 : 能够合批的元素一定是紧挨着的,UI1与UI2可以合并,但是UI1与UI3是不能合批的,因为UI2打断了合批
因此UI的渲染顺序是非常重要的,防止合批被打断
深度越小越先渲染
1).从第一个white物体开始判断depth是否为-1(物体是否显示),未显示则继续判断下一个物体depth,显示则判断其是否覆盖在其他物体上,由上图可知white没有覆盖在其他物体之上,因此其depth为0
2).text覆盖在white上,text比较特殊的是框有可能比mesh的大
所以这里注意覆盖的条件不是外面的框重叠,而是要看两个UI的mesh是否重叠
如果覆盖,判断两者是否能够合批(Material和Texture是否是一样的),这里很明显不能合批,因此text的depth的深度值 = white深度值+1 = 1
如果覆盖了很多个,那么text的深度值 = 覆盖元素中深度值最大的+1
3).red覆盖在white和text之上,且red与text并不能合批,text与white深度值较大的是text,因此red depth = text depth + 1 = 2
4).yellow覆盖在red之上,且能合批,其深度为2(注意这里只是合批测试,不是真正的进行合批操作)
5).blue覆盖在yellow之上,且能合批,其深度为2
6).根据深度得到的数组中有三个深度为2的,这时再对深度相同的进行排序,Material ID、Texture ID都相同,最后根据Render Order排序,因此最后的数组是 : white - text - red - yellow - blue
7).数组会传入到批处理部分,传入之后从第一个元素开始判断是否能与相邻元素进行判断是否能够进行合批(真正执行合批的地方),W与T不能合批,其批次号为0,同理T批次号为1,R与Y能合批,继续判断下一个B也能合批,因此R Y B批次号为2
总结:
1).遍历所有UI元素
2).根据UI Depth、Material ID、Texture ID、Render Order排序得出数组
3).筛选掉所有深度值为-1的UI元素
4).判断相邻元素是否进行合批,得到批次号
Gameobject Count : 当前批次所含有的gameobject数量
合批结果 :text,white,blue三个批次,因为排序后的数组是W -T -B,这是因为W的texture ID更小一些,所以T与W排序时W排在了前面,解决方法是给W与B的image组件赋同一个texture,这样T就不会打断W与B的合批
1.设置模板缓存
给mask添加一个特殊的材质,这也是为什么会打断合批的原因,因为材质不一样,将需要隐藏的像素的模板缓存值设置为0,需要显示的像素模板缓存值设置为1,这是第一次drawcall
2.遍历mask下的子物体后会还原模板缓存,因此又产生了一次drawcall
材质完成子物体的渲染之后,判断像素的模板缓存值是否为1而决定是否渲染,进行还原是第二次drawcall
因为mask产生两次drawcall,两个mask可以在设置的drawcall进行合批,还原的drawcall进行合批,总得来说还是两个drawcall
注意 :
1).相机会产生一个drawcall
2).mask组件挂在的物体上必须有image组件,否则不会生效
3).被遮挡的物体依然是绘制的,会占一次drawcall,只是mask将绘制的像素剔除了
4).mask下的子物体可以正常进行合批的
5).两个mask可以进行合批,这两个可以合批的mask的子物体之间也可以进行合批
6).当下一个mask与上一个mask隐藏的子物体有覆盖现象时,因为上一个mask隐藏的子物体打断了两个mask的合批,因此下一个的mask会单独计算drawcall,无法进行合批
比mask更节省性能
1).RectMask2D本身不占用drawcall,实际只是用当前物体的区域对子物体进行裁剪,本身是不占用drawcall
2).mask中完全被裁剪的物体顶点和面还是正常的绘制,但是RectMask2D完全被裁减的物体是不会绘制顶点和面的,也不会占用drawcall
3).RectMask2D完全被裁减的物体不参与其他UI之间的深度运算,不能合批
4).RectMask2D下的子物体可以合批,也可以跟单独的物体进行合批(上面没有RectMask2D限制),但是与另一个RectMask2D下的子物体不能合批
与mask相比 :
RectMask2D最大的问题是两个RectMask2D之间无法合批,因此根据使用场景的不同应该选择不同的遮罩方式
1.有多个遮罩,并且遮罩下的子物体之间是可以合批的选择使用mask
2.如果只有一个mask并且只是UI的遮罩,使用RectMask2D更节省性能
一个像素被绘制的次数越多,颜色就越亮
尽量避免UI覆盖,也避免内容全部分开
UI图片使用异形图片,因为这样可以减少有效区域,减少覆盖,比如一个圆形图片,一般会切成长方形,周围无需显示的是透明区域,但是这样会增加有效区域,增加UI覆盖的可能性
Image组件需要设置image type :
不规则镂空需要使用脚本,后续详解
比如outline和shadow,会增加大量的顶点和面数,实现效果也不理想
将数值改大点可以看到左边有四个new text,也就是说他为了实现描边效果增加了4倍的顶点和面数
可采用Text Mesh Pro插件实现描边等效果