目录
Screen Space - Overlay
Screen Space - Camera
World Space
UI合批分析(建议不看 直接看FrameDebugger测试)
优化UI合批
1、Image图片纹理不同导致合批失败
2、文本和图片相交以及排序对合批的影响
3、Mask对合批的影响(情况很复杂)
4、Canvas对合批的影响
参考文档:画布 - Unity 手册
Canvas组件:画布组件是进行 UI 布局和渲染的抽象空间。所有 UI 元素都必须是附加了画布组件的游戏对象的子对象。
Screen Space - Overlay
参数:
Render Mode 渲染模式:Screen Space - Overlay、Screen Space - Camera、World Space。
Pixel Perfect:是否应该无锯齿精确渲染 UI?
Sort Order:渲染层级
Target Display:输出屏幕目标Display 1
Additional Shader Channels:额外的着色器通道
画布渲染于所有物体上方,即最后渲染。并且不归于摄像机渲染,因此即使没有摄像机也能渲染出UI。
画布宽高跟随屏幕宽高,画布大小固定(1,1,1),位置屏幕居中,覆盖整个屏幕。
画布下的子UI需使用锚点适配来适应不同大小的屏幕,因屏幕变化后画布也会变化。
Screen Space - Camera
画布内容归于摄像机进行渲染。与正常渲染物体一样。
画布宽度和高度跟随屏幕。大小会根据UI摄像机参数而适配变化。
Render Camera: UI摄像机
Plane Distance:画布距离UI摄像机的长度
Sorting Layer:可自定义大层级
Order In Layer:子层级
观察Frame Debugger分析
由于有2个摄像机,因此有2个Render.OpaqueGeometry以及2个Camera.RenderSkybox。若将UI摄像机的Clear Flags从Skybox改为DepthOnly,则会减少1个Camera.RenderSkybox。
UI渲染主要位于Render.TransparentGeometry中
Image默认材质着色器UI/Default会将渲染目标交到TempBuffer 355 1920*1080目标,着色器是支持SrcAlpha OneMinusSrcAlpha的常见透明因子混合,并存在深度测试小于等于(<=),不写入深度。(一般情况透明物体是不开启深度测试的,而这里开启的目的是为了能被3D物体遮挡)
规范做法:
主摄像机屏蔽UI层渲染
UI摄像机仅渲染UI层
将3D物体设置到UI层
此时若想把Cube渲染在UI之上,那么就是直接放到Canvas物体前面即可。
Canvas距离UI摄像机的距离由下图参数Plane Distance决定(默认100)
若放在Canvas后面则是被遮挡。
UI摄像机不一定是正交的,即使换成透视视角,依然是保持正常的UI显示(画布会缩放大小)并3D物体以透视视角渲染出来。
但透视视角会有更大的开销用于裁剪,一般情况下都是正交视角节省开销。
注意事项:不要试图用主摄像机去渲染在参与UI排序的3D物体,若使用主摄像机渲染,这个3D物体是绝对位于UI之下的,因为主摄像机的深度缓冲区被UI摄像机清空了,UI摄像机开始渲染时所有UI像素都会正常通过深度测试,所以就肯定会渲染在3D物体之上。正常就应该是交给UI摄像机渲染,UI摄像机渲染时,正常3D物体会先被渲染,深度写入后,UI物体再参与渲染时就会正常通过深度测试将被3D物体遮挡的像素过滤掉,呈现出3D物体在UI之上的。
World Space
它同样可以指定一个摄像机专门负责渲染画布。但区别于Screen Space - Camera,画布的位置、旋转、缩放均不会随着屏幕、摄像机变化而变化,它就变成和普通的3D平面物体一样看待。
UI合批分析(建议不看 直接看FrameDebugger测试)
实际上我们不太需要这个合批分析工具,因为太难理顺各种UGUI的合批规则了,而且很多参数都很难获取到,即使获取到了也不一定准确,总之最好的分析工具还是打开Frame Debugger 然后手动调下就好了。 (已放弃维护)
原理及工具参考:【Unity】静态优化工具支持UGUI合批分析、AB包冗余分析、预制体使用资源情况分析_ab包资源冗余-CSDN博客
合批分析使用必须运行游戏之后再使用才正常,因为有很多数据都是运行后Unity才正常设置的,比如图集纹理,不运行是不会使用图集的。
合批工具问题:
1、相交算法有问题,原本用的是rect1.position 实际要用rect1.localPosition,兼容锚点和中心点不是中心的情况。
2、解决问题1后,其实发现要真正计算相交的是网格,而不是RectTransform区域,不同组件的网格获取方式不同,如图片、文本,其他组件还未支持,比如Spine等等,所以这个工具很难维护...
RectTransformExtensions.cs 代码修改和新增如下部分。
public static void CalcualteGraphicRect(Graphic graphic, ref Rect rect)
{
Vector2 size = Vector2.zero;
Text textComponent = graphic as Text;
if (textComponent != null)
{
List<UIVertex> textUIVertexList = new List<UIVertex>();
textUIVertexList = textComponent.cachedTextGenerator.verts as List<UIVertex>;
//textComponent.cachedTextGenerator.GetVertices(textUIVertexList);
float minX = float.MaxValue;
float maxX = float.MinValue;
float minY = float.MaxValue;
float maxY = float.MinValue;
//使用center计算出中心点后,你还需要转空间,这个转空间一直出问题,所以不采用这种办法
//Vector3 center = Vector3.zero;
Vector3 minPos = Vector3.zero;
Vector3 maxPos = Vector3.zero;
string str = "";
for (int i = 0; i < textUIVertexList.Count; i++)
{
Vector3 pos = textUIVertexList[i].position;
minX = Mathf.Min(minX, pos.x);
maxX = Mathf.Max(maxX, pos.x);
minY = Mathf.Min(minY, pos.y);
maxY = Mathf.Max(maxY, pos.y);
minPos = Vector3.Min(minPos, pos);
maxPos = Vector3.Max(maxPos, pos);
//center += pos;
str += pos + " ";
}
size.x = maxX - minX;
size.y = maxY - minY;
//center /= textUIVertexList.Count;
//转空间会有问题 不采用计算中心点 而是直接算出以文本空间(中心点是文本坐标点rect.position)的网格中心点。
//转空间就是直接用 网格中心点 + 文本坐标点,将网格中心点转到 文本所在空间(Canvas空间系下) 将其作为Rect区域的中心点去计算网格相交
Vector2 rawPos = rect.position;
//解决偏移问题,根据MinX MaxX MinY MaxY 求出局部中心点, 用文本中心点rect.position加上局部中心点就能得到真正的文本网格中心点
Vector2 offset = new Vector2(((minX + maxX) / 2.0f), ((minY + maxY) / 2.0f));
rect.position += offset;
Debug.Log("rawPos:" + rawPos + " afterPos:" + rect.position + "offset:" + offset);
Debug.Log(graphic.gameObject.name + " " + size + "\n" + str);
}
else
{
Image imageComponent = graphic as Image;
RectTransform r = imageComponent.GetComponent<RectTransform>();
//只能编辑器读资源来获取MeshType,其他方式都试过不行...
string path = AssetDatabase.GetAssetPath(imageComponent.sprite);
TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
TextureImporterSettings settings = new TextureImporterSettings();
textureImporter.ReadTextureSettings(settings);
//if (imageComponent.overrideSprite.packingMode == SpritePackingMode.Tight) //不能用这个packingMode判断, 会严格按照Sprite的MeshType和图集的Tight Packing决定。
if (settings.spriteMeshType == SpriteMeshType.Tight)
{
Vector3[] worldPosArr = new Vector3[4];
r.GetWorldCorners(worldPosArr);
float minX = float.MaxValue;
float maxX = float.MinValue;
float minY = float.MaxValue;
float maxY = float.MinValue;
string str = "";
for (int i = 0; i < worldPosArr.Length; i++)
{
Vector3 pos = worldPosArr[i];
minX = Mathf.Min(minX, pos.x);
maxX = Mathf.Max(maxX, pos.x);
minY = Mathf.Min(minY, pos.y);
maxY = Mathf.Max(maxY, pos.y);
str += pos + " ";
}
size.x = (maxX - minX) * 100;
size.y = (maxY - minY) * 100;
Debug.Log("[Tight] " + graphic.gameObject.name + " " + size + "\n" + str);
}
else
{
size = r.rect.size;
Debug.Log("[FullRect]" + graphic.gameObject.name + " " + size);
}
}
rect.size = size;
}
/// <summary>
/// 适用
/// </summary>
/// <param name="rect1"></param>
/// <param name="rect2"></param>
/// <returns></returns>
public static bool IsRectTransformOverlap(RectTransform rect1, RectTransform rect2)
{
//获取真实网格大小宽度高度
Vector2 size1 = rect1.rect.size;
Vector2 size2 = rect2.rect.size;
Vector2 pos1 = rect1.localPosition;
Vector2 pos2 = rect2.localPosition;
Rect r1 = new Rect(pos1, size1);
Rect r2 = new Rect(pos2, size2);
Graphic g1 = rect1.GetComponent<Graphic>();
Graphic g2 = rect2.GetComponent<Graphic>();
if (g1 as Text || g1 as Image)
{
CalcualteGraphicRect(g1, ref r1);
}
if (g2 as Text || g2 as Image)
{
CalcualteGraphicRect(g2, ref r2);
}
Debug.Log(rect1.gameObject.name + " rect1: " + r1);
Debug.Log(rect2.gameObject.name + " rect2: " + r2);
float rect1MinX = r1.x - r1.width / 2;
float rect1MaxX = r1.x + r1.width / 2;
float rect1MinY = r1.y - r1.height / 2;
float rect1MaxY = r1.y + r1.height / 2;
//Debug.Log($"r1 pos:{pos1}, rect:{rect1.rect}");
float rect2MinX = r2.x - r2.width / 2;
float rect2MaxX = r2.x + r2.width / 2;
float rect2MinY = r2.y - r2.height / 2;
float rect2MaxY = r2.y + r2.height / 2;
//Debug.Log($"r2 pos:{pos2}, rect:{rect2.rect}");
bool xNotOverlap = rect1MaxX <= rect2MinX || rect2MaxX <= rect1MinX;
//Debug.Log($"r1 maxX:{rect1MaxX}, r2 minx:{rect2MinX}, rect1MaxX <= rect2MinX:{rect1MaxX <= rect2MinX}");
//Debug.Log($"r2 maxX:{rect2MaxX}, r1 minx:{rect1MinX}, rect2MaxX <= rect1MinX:{rect2MaxX <= rect1MinX}");
//Debug.Log($"xNotOverlap:" + xNotOverlap);
bool yNotOverlap = rect1MaxY <= rect2MinY || rect2MaxY <= rect1MinY;
//Debug.Log($"r1 maxY:{rect1MaxY}, r2 minY:{rect2MinY}, rect1MaxY <= rect2MinY:{rect1MaxY <= rect2MinY}");
//Debug.Log($"r2 maxY:{rect2MaxY}, r1 minY:{rect1MinY}, rect2MaxY <= rect1MinY:{rect2MaxY <= rect1MinY}");
//Debug.Log($"yNotOverlap:" + yNotOverlap);
bool notOverlap = xNotOverlap || yNotOverlap;
return !notOverlap;
}
图片网格就是注意Tight紧凑(Mesh Type)情况,网格区域是比RectTransform小的(大部分情况),而文本网格则是肯定会比RectTransform小,而且要特别注意不要只盯着一个文本符号网格的高度看,要看整体符号网格,比如下图:你只有发现全部都没相交图片网格才是不相交,否则只要有1个相交了,那就打断合批(节点交叉摆放情况才会,下面优化部分说明)
优化UI合批
1、Image图片纹理不同导致合批失败
0/0/0/0 代表 合批ID/深度/材质ID/贴图ID(仅需关注合批ID)
解决办法:将2张图片使用图集合并成一张图集图片(SpriteAtlas)然后Unity就会是使用图集图片去渲染,而不是2张图片,至于具体渲染图集图片的那张图片由Unity分析出图片所在图集图片的UV范围而采集纹理。
需要开启Sprite Packer Mode是Always Enabled
位于Project Settings - Editor
2、文本和图片相交以及排序对合批的影响
仅有文本放到图片上且网格相交会打断文本合批,如果文本位于图片下面(被图片遮挡)则不会有影响。
原本文本和图片网格不相交(注意是网格,不是RectTransform的区域Rect)
一旦相交网格,就会打断合批,检查发现打断的是文本合批,图片合批正常执行。
其实也就是常见的按钮就是文本放到了图片上面的,如果有多个会如下。。。
解决办法:将文本全部挪到图片节点的下面,不要出现交叉图片和文本节点的形式。(图文节点交叉形式)
3、Mask对合批的影响(情况很复杂)
Show Mask Graphic:是否显示裁剪图片,勾选显示,否则不显示。这个不会影响裁剪、以及裁剪合批情况。
情况一:正常裁剪(Mask与其他Mask或图片不相交;Mask内图片与其他不相交)(渲染4次)
一共2个图集图片Image、其中一个图片(树)是白色图片Mask的子物体被裁剪。
Mask裁剪会将Mask内与Mask外图片合批打断,所以2张图片需要2次渲染;
1个Mask组件自身会进行一次Mask遮罩模板写入渲染,一次Mask遮罩模板清除渲染,共2次。
每个Mask组件之间的这个模板写入渲染和模板清除渲染也是可以合批的,稍后说明。
FrameDebugger情况如下:
自上而下分别是:
Mask图片自身渲染+Mask模板写入渲染
花图片渲染
树图片渲染
Mask模板清除渲染
情况二:将花图片拖拽到树和Mask图片的位置,不过花图片层级是低于树和Mask图片的,与情况一相比没有任何合批影响。(即不会增加DC次数)(渲染4次)
但是,渲染顺序自上而下变化:
花
Mask写入
树
Mask清除
情况三:将花图片也使用一个新的Mask图片裁剪(渲染3次)
2个Mask模板写入(合批进行)、2个Mask下同图集图片合批、2个Mask模板清除(合批进行)共3次渲染。也就是增加Mask反而能让DC下降。
情况四:花图片网格与其他Mask图片网格相交时,合批将发生巨大变化,由3变6(渲染6次)
严格来说是低层Mask下的图片网格与高层Mask网格相交
自上而下:
花Mask裁剪模板写入
花图片
树Mask裁剪模板写入
花Mask裁剪模板清除
树图片
树Mask裁剪模板清除
也就是说,全部都没有进行合批,目前已知情况三是最好的,情况四是最差的。
情况五:将花的Mask网格与另一个Mask下的树网格相交。(渲染5次)
严格来说是低层Mask网格与高层Mask下的图片网格相交
自上而下:
2个Mask模板写入
花
花Mask模板清除
树
树Mask模板清除
情况六:花的Mask网格与树的Mask网格相交(渲染6次)
自上而下:
花Mask裁剪模板写入
花图片
花Mask裁剪模板清除
树Mask裁剪模板写入
树图片
树Mask裁剪模板清除
情况七:将花转移到树的Mask下(作为子物体),花的Mask图片物体删除。(渲染3次)
只要Mask下的图片网格与Mask网格相交,则都会合批图片。
自上而下:Mask模板写入、图集图片合批、Mask模板清除
情况八:在情况七基础上,将花的网格不与Mask网格相交(将花移动到Mask裁剪区域之外)(渲染4次)
自上而下:花、mask写入、树、mask清除
情况九:将Mask下的所有图片移动到Mask网格之外。(渲染3次)
自上而下:mask写入、图集图片合批、mask清除
总结:情况三、情况七、情况九是最好的,仅3次,除了情况四、情况五、情况六是比较差的。
需要着重处理比较差的情况,往最好的情况去处理。
4、Canvas对合批的影响
与Mask的影响类似,但不会有Canvas自身的渲染(例如:模板写入、模板清除操作)但它会打断非Canvas内的上一个物体与下一个物体的合批。而每个Canvas内的元素合批情况与原本一样。
自上而下:(最差的情况)
花Canvas_Flwoer(1)
Canvas_Flower(1) 的 文本1 Text(1)
Canvas_Flower(1) 的 Image
Canvas_Flower(1) 的 文本2 Text(2)
树 Tree
花Canvas_Flower(2)
花Canvas_Flower(2) 的 文本 Text
树 Tree(1)
可发现Canvas和Canvas之间的Tree虽然是同样的图片,但没有合批,因为被Canvas打断,而Canvas_Flower(1)内的文本2个也没有合批,因为被Image打断。
优化后如下:
不要有交叉Canvas和Canvas之外物体的节点情况。
Canvas内的直接使用原本优化UI合批方法进行优化即可,即将文本和图片节点不要出现交叉。