|
前言
了解ugui网格重建原理,是进行ugui优化的基础。 什么时候才会网格重建?
首先抱着问题去找答案:什么时候才会网格重建? 我们以Graphic为切入点,了解一下图片什么时候会进行网格重建。在Graphic源码中,把所有函数一折叠,映入眼帘的第一个函数就是我们要找的答案。
public virtual void SetAllDirty()
{
SetLayoutDirty();
SetVerticesDirty();
SetMaterialDirty();
}
由于元素的改变可分为布局变化、顶点变化、材质变化,所以分别提供了三个方法SetLayoutDirty();SetVerticesDirty();SetMaterialDirty();供选择。
SetLayoutDirty
protected override void OnRectTransformDimensionsChange()
{
if (gameObject.activeInHierarchy)
{
// prevent double dirtying...
if (CanvasUpdateRegistry.IsRebuildingLayout())
SetVerticesDirty();
else
{
SetVerticesDirty();
SetLayoutDirty();
}
}
}
public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
//加入重建队列
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
if (m_OnDirtyLayoutCallback != null)
m_OnDirtyLayoutCallback();
}
OnRectTransformDimensionsChange是指当UI的RectTransform更改时的回调,只要继承UIBehavior即可获取回调。
所以只要RectTransform的属性发生变化,都会导致网格重建:
position、width、height、anchors、pivot、rotation、scale SetVerticesDirty
拿Image举个例子:
Image.cs
public bool preserveAspect { get { return m_PreserveAspect; }
set { if (SetPropertyUtility.SetStruct(ref m_PreserveAspect, value)) SetVerticesDirty(); } }
public bool fillCenter { get { return m_FillCenter; }
set { if (SetPropertyUtility.SetStruct(ref m_FillCenter, value)) SetVerticesDirty(); } }
public FillMethod fillMethod { get { return m_FillMethod; }
set { if (SetPropertyUtility.SetStruct(ref m_FillMethod, value)) { SetVerticesDirty(); m_FillOrigin = 0; } } }
public float fillAmount { get { return m_FillAmount; }
set { if (SetPropertyUtility.SetStruct(ref m_FillAmount, Mathf.Clamp01(value))) SetVerticesDirty(); } }
public bool fillClockwise { get { return m_FillClockwise; }
set { if (SetPropertyUtility.SetStruct(ref m_FillClockwise, value)) SetVerticesDirty(); } }
public int fillOrigin { get { return m_FillOrigin; }
set { if (SetPropertyUtility.SetStruct(ref m_FillOrigin, value)) SetVerticesDirty(); } }
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
能导致image顶点变化的有好多元素:
preserveAspect 、fillCenter 、fillMethod 、fillAmount 、fillClockwise 、fillOrigin Graphic中设置颜色Color也会导致顶点脏数据导致网格重建
Graphic.cs
public virtual Color color { get { return m_Color; }
set { if (SetPropertyUtility.SetColor(ref m_Color, value)) SetVerticesDirty(); } }
SetMaterialDirty
public virtual Material material
{
get
{
return (m_Material != null) ? m_Material : defaultMaterial;
}
set
{
if (m_Material == value)
return;
m_Material = value;
SetMaterialDirty();
}
}
public virtual void SetMaterialDirty()
{
if (!IsActive())
return;
m_MaterialDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyMaterialCallback != null)
m_OnDirtyMaterialCallback();
}
这个就没什么好说的,一旦材质变化了,肯定要网格重建的。
这3个方法,最终都调用了CanvasUpdateRegistry方法,
- SetLayoutDirty最终注册到m_LayoutRebuildQueue
- SetVerticesDirty和SetMaterialDirty最终都注册到m_GraphicRebuildQueue
UI发生变化一般分两种情况,一种是修改了宽高这样会影响到顶点位置需要重建Mesh; 还有一种仅仅只修改了显示元素,这样并不会影响顶点位置,此时unity会在代码中区别对待。CanvasUpdateRegistry维护了2个队列来管理这两种ui变化需要的网格重建
private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
但其实我是有点没看懂两层遍历的外层循环,···
下面汇总一下CanvasUpdate定义的用处,其实这些定义对应不同的组件去做Rebuild操作,
- Graphic:CanvasUpdate.PreRender
- InputField:CanvasUpdate.LatePreRender
- ScrollRect:CanvasUpdate.Prelayout==>UpdateCachedData
- CanvasUpdate.PostLayout==>UpdateBounds
public class CanvasUpdateRegistry
{
//...略
protected CanvasUpdateRegistry()
{
//构造函数处委托函数到PerformUpdate()方法中
//每次Canvas.willRenderCanvases就会执行PerformUpdate()方法
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
//开始BeginSample()
//在Profiler中看到的标志性函数Canvas.willRenderCanvases耗时就在这里了
//EndSample()
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
//需要重建的布局元素(RectTransform发生变化),首先需要根据子对象的数量对它进行排序。
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
//遍历待重建布局元素队列,开始重建
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = instance.m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);//重建布局元素
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue.LayoutComplete();
//布局构建完成后清空队列
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
// 布局构建结束,开始进行Mask2D裁切(详细内容下面会介绍)
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
//需要重建的Graphics元素(Image Text RawImage 发生变化)
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);//重建UI元素
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
}
}
}
//这里需要思考的是,有可能一个Image对象,RectTransform和Graphics同时发生了修改,它们的更新含义不同需要区分对待
//1.修改了Image的宽高,这样Mesh的顶点会发生变化,此时该对象会加入m_LayoutRebuildQueue队列
//2.修改了Image的Sprite,它并不会影响顶点位置信息,此时该对象会加入m_GraphicRebuildQueue队列
//所以上面代码在遍历的时候会分层
//for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
//for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
//Rebuild的时候会把层传进去,保证Image知道现在是要更新布局,还是只更新渲染。
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue.GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
}
想要更详细地了解什么元素导致了网格重建,可以看一下雨松的文章UI重建触发事件提取
<hr/>网格重建
了解了什么时候网格会重建,接着我们来了解一下网格重建都做了什么操作。
网格重建的时候会调用元素的Rebuild方法,以Graphic为例:Graphic的rebuild方法中主要有2个函数UpdateGeometry、UpdateMaterial。
Graphic.cs
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer.cull)
return;
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
//更新网格顶点信息
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
//更新渲染信息
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}
UpdateGeometry(更新几何网格),就是确定每一个UI元素Mesh的信息,包括顶点数据、三角形数据、UV数据、顶点色数据。
protected virtual void UpdateGeometry()
{
//Image RawImage Text 在构造函数都给 useLegacyMeshGeneration 赋值为false
if (useLegacyMeshGeneration)
DoLegacyMeshGeneration();
else
DoMeshGeneration(); //更新Image RawImage Text 元素网格信息
}
private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
OnPopulateMesh(s_VertexHelper);//在继承类中实现具体的元素信息
else
s_VertexHelper.Clear();
var components = ListPool<Component>.Get();
//获取当前对象是否有IMeshModifier接口
//Text的描边和阴影都是通过IMeshModifier的ModifyMesh()实现出来的
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);//提交网格信息,开始合并网格
}
顶点数据准备完毕后会调用canvasRenderer.SetMesh()方法来提交。很遗憾CanvasRenderer.cs并没有开源,我们只能继续反编译看它的实现了,SetMesh()方法最终在C++中实现,毕竟由于UI的元素很多,同时参与合并顶点的信息也会很多,在C++中实现效率会更好。看到这里,我相信大家应该能明白UGUI为什么效率会比NGUI要高一些了,因为NGUI的网格Mesh合并都是在C#中完成的,而UGUI网格合并都是在C++中底层中完成的。
UpdateMaterial(更新渲染信息),就是把mainTexture设置渲染显示!
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
} |
|