UGUI笔记——UI Mesh Rebuild

1.0 UGUI原理

在讲解UI网格重建之前,我们先简单了解一下UGUI实现原理,首先要显示UI,就要生成显示UI用的Mesh,如图所示,一个矩形的Mesh,由4个顶点,2个三角形组成,每个顶点都包含UV坐标,如果需要调整颜色,还需要提供顶点色。例如,调节Image或者Text的颜色,其实就是改变它们的顶点色而已。


网格

然后将网格和纹理信息发送给GUI中,进行渲染,这样一个简单的UI元素就显示出来了,其实这个流程与渲染一个普通的Cube,是类似的。可以简单的理解为,所谓的UI其实就是用一个正交的Camera看着若干的平面网格。不过,只是单单的显示出来还远远不够,例如DrawCall需要合并,Button需要点击等等,因此就诞生了UGUI系统,下面我们通过源码来具体看看UGUI是如何绘制UI Mesh的。

2.0 顶点辅助类VertexHelper

我们在UGUI的原理中说过想要显示一个UI就需要对应的Mesh信息,那么UI的Mesh信息(顶点,三角形等)是保存到了哪里呢?这个就是VertexHelper的作用,它只是一个普通的类对象,保存了生成Mesh的基本信息,如每个顶点的位置,颜色,UV,法线,切线,三角形索引及其生成Mesh的方法,而并非Mesh对象,我们可以通过这些信息生成对应的Mesh网格。

VertexHelper.cs部分源码如下:

    public class VertexHelper : IDisposable
    {
        private List m_Positions;
        private List m_Colors;
        private List m_Uv0S;
        private List m_Uv1S;
        private List m_Uv2S;
        private List m_Uv3S;
        private List m_Normals;
        private List m_Tangents;
        private List m_Indices;

        private static readonly Vector4 s_DefaultTangent = new Vector4(1.0f, 0.0f, 0.0f, -1.0f);
        private static readonly Vector3 s_DefaultNormal = Vector3.back;

        private bool m_ListsInitalized = false;

        public void FillMesh(Mesh mesh)
        {
            InitializeListIfRequired();

            mesh.Clear();

            if (m_Positions.Count >= 65000)
                throw new ArgumentException("Mesh can not have more than 65000 vertices");

            mesh.SetVertices(m_Positions);
            mesh.SetColors(m_Colors);
            mesh.SetUVs(0, m_Uv0S);
            mesh.SetUVs(1, m_Uv1S);
            mesh.SetUVs(2, m_Uv2S);
            mesh.SetUVs(3, m_Uv3S);
            mesh.SetNormals(m_Normals);
            mesh.SetTangents(m_Tangents);
            mesh.SetTriangles(m_Indices, 0);
            mesh.RecalculateBounds();
        }
        internal void AddVert(Vector3 position, Color32 color, Vector2 uv0, Vector2 uv1, Vector2 uv2, Vector2 uv3, Vector3 normal, Vector4 tangent)
        {
            InitializeListIfRequired();

            m_Positions.Add(position);
            m_Colors.Add(color);
            m_Uv0S.Add(uv0);
            m_Uv1S.Add(uv1);
            m_Uv2S.Add(uv2);
            m_Uv3S.Add(uv3);
            m_Normals.Add(normal);
            m_Tangents.Add(tangent);
        }
        public void AddTriangle(int idx0, int idx1, int idx2)
        {
            InitializeListIfRequired();

            m_Indices.Add(idx0);
            m_Indices.Add(idx1);
            m_Indices.Add(idx2);
        }
    }

3.0 Canvas.WillRenderCanvases事件

通过官方文档,我们可以了解到,当Canvas需要重绘时会调用Canvas.SendWillRenderCanvases()方法,遗憾的是UGUI并没有公开Canvas的源码,通过反编译我们可以看到Canvas的部分源码如下:
Canvas部分源码如下:

  [NativeClass("UI::Canvas")]
  [NativeHeader("Runtime/UI/Canvas.h")]
  [NativeHeader("Runtime/UI/UIStructs.h")]
  public sealed class Canvas : Behaviour
  {
    public delegate void WillRenderCanvases();
    public static event Canvas.WillRenderCanvases willRenderCanvases;

    public static void ForceUpdateCanvases() => Canvas.SendWillRenderCanvases();

    [RequiredByNativeCode]
    private static void SendWillRenderCanvases()
    {
      if (Canvas.willRenderCanvases == null)
        return;
      Canvas.willRenderCanvases();
    }
  }

SendWillRenderCanvas()方法中调用Canvas.willRenderCanvases()事件,因此我们可以监听该事件,来刷新我们自己的UI系统,那么我们就来看看UGUI是如何做的。

4.0 CanvasUpdateRegistry

在UGUI中,在CanvasUpdateRegistry的构建函数中,可以看到Canvas.willRenderCanvases事件添加到了PerformUdpate()方法中,然后重新绘制m_LayoutRebuildQueue和m_GraphicRebuildQueue这俩个集合中的UI元素。

CanvasUpdateRegistry.cs部分源码如下:

    public class CanvasUpdateRegistry
    {
        private readonly IndexedSet m_LayoutRebuildQueue = new IndexedSet();
        private readonly IndexedSet m_GraphicRebuildQueue = new IndexedSet();

        protected CanvasUpdateRegistry()
        {
            Canvas.willRenderCanvases += PerformUpdate;
        }

        private static readonly Comparison s_SortLayoutFunction = SortLayoutList;
        private void PerformUpdate()
        {
            UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
            CleanInvalidItems();

            m_PerformingLayoutUpdate = true;

            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[i].LayoutComplete();

            instance.m_LayoutRebuildQueue.Clear();
            m_PerformingLayoutUpdate = false;

            // now layout is complete do culling...
            ClipperRegistry.instance.Cull();

            m_PerformingGraphicUpdate = true;
            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);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
                    }
                }
            }

            for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
                m_GraphicRebuildQueue[i].GraphicUpdateComplete();

            instance.m_GraphicRebuildQueue.Clear();
            m_PerformingGraphicUpdate = false;
            UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
        }
  }
  • m_LayoutRebuildQueue:保存着需要重建的布局元素(一般是通过LayoutGroup布局改变的UI)
  • m_GraphicRebuildQueue:需要重建的Graphics元素(如Image,Text,RawIamge的贴图,材质,宽高发生变化)
  • UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
    我们可以在Profiler中通过查看标志性函数Canvas.willRenderCanvases的耗时,来了解Mesh重建的性能消耗。
  • ClipperRegistry.instance.Cull();
    布局重建结束后,开始进行Mask2D裁切,这个将在其他文章中介绍。

5.0 ICanvasElement.Rebuild()

通过上面的源码,我们可以看到Canvas.willRenderCanvases事件实际撒花姑娘是调用了每个ICanvasElement接口下的Rebuild方法。
UGUI的布局系统是通过LayoutRebuilder类管理的,LayoutRebuilder实现了ICanvasElement接口,用于管理layout的rebuilding,实现自动布局。除此之外,UGUI的Image和Text组件都是派生自Graphics类,并且实现了ICanvasElement接口,用于最终更新Mesh。这也分别对应了上面的m_LayoutRebuildQueue和m_GraphicRebuildQueue俩个集合中的元素。

5.1 Graphics的Rebuild()方法

我们先来看看Graphics的Rebuild()方法,Rebuild()方法会调用UpdateGeometry()用于更新几何网格,调用UpdateMaterial()用于更新材质,这与我们讲的UI绘制原来一样。

Graphics.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;
            }
        }

5.1.1 UpdateGeometry()

Graphic中有个静态对象s_VertexHelper保存每次生成的Mesh信息(包括顶点,三角形索引,UV,顶点色等数据),使用完后会立即清理掉等待下个Graphic对象使用。

Graphic.cs部分源码如下:

    public abstract class Graphic: UIBehaviour,ICanvasElement
    {
        [NonSerialized] protected static Mesh s_Mesh;
        [NonSerialized] private static readonly VertexHelper s_VertexHelper = new VertexHelper();
        protected static Mesh workerMesh
        {
            get
            {
                if (s_Mesh == null)
                {
                    s_Mesh = new Mesh();
                    s_Mesh.name = "Shared UI Mesh";
                    s_Mesh.hideFlags = HideFlags.HideAndDontSave;
                }
                return s_Mesh;
            }
        }
        /// 
        /// Call to update the geometry of the Graphic onto the CanvasRenderer.
        /// 
        protected virtual void UpdateGeometry()
        {
            if (useLegacyMeshGeneration)
                DoLegacyMeshGeneration();
            else
                DoMeshGeneration();
        }
        private void DoMeshGeneration()
        {
            if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
                OnPopulateMesh(s_VertexHelper);
            else
                s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.

            var components = ListPool.Get();
            GetComponents(typeof(IMeshModifier), components);

            for (var i = 0; i < components.Count; i++)
                ((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);

            ListPool.Release(components);

            s_VertexHelper.FillMesh(workerMesh);
            canvasRenderer.SetMesh(workerMesh);
        }
    }

我们可以看到,s_VertexHelper中的数据通过OnPopulateMesh函数,进行填充,它是一个虚函数会在各自的类中实现,如下是默认的网格数据,我们可以在自己的UI类中,重写OnPopulateMesh方法,实现自定义的UI。

        protected virtual void OnPopulateMesh(VertexHelper vh)
        {
            var r = GetPixelAdjustedRect();
            var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);

            Color32 color32 = color;
            vh.Clear();
            vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f));
            vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f));
            vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f));
            vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f));

            vh.AddTriangle(0, 1, 2);
            vh.AddTriangle(2, 3, 0);
        }

s_VertexHelper数据填充之后,调用FillMesh()方法生成真正的Mesh,然后调用 canvasRenderer.SetMesh()方法来提交。很遗憾CanvasRenderer.cs并没有开源,通过反编译看到如下代码,SetMesh()方法最终在C++中实现,这也是UGUI的效率比NGUI高一些的原因,因为NGUI的Mesh合并是在C#中完成的,而UGUI的Mesh合并是在C++中底层完成的。

CanvasRenderer.cs 部分源码如下:

    [MethodImpl(MethodImplOptions.InternalCall)]
    public extern void SetMesh(Mesh mesh);

5.1.2 UpdateMaterial()

UpdateMaterial()方法会通过canvasRenderer来更新Material与Texture。

        /// 
        /// Call to update the Material of the graphic onto the CanvasRenderer.
        /// 
        protected virtual void UpdateMaterial()
        {
            if (!IsActive())
                return;
            canvasRenderer.materialCount = 1;
            canvasRenderer.SetMaterial(materialForRendering, 0);
            canvasRenderer.SetTexture(mainTexture);
        }

很遗憾CanvasRenderer.cs并没有开源,通过反编译看到如下代码。

CanvasRenderer.cs 部分源码如下:

    [MethodImpl(MethodImplOptions.InternalCall)]
    public extern void SetMaterial(Material material, int index);
    [MethodImpl(MethodImplOptions.InternalCall)]
    public extern void SetTexture(Texture texture);

5.2 LayoutRebuilder的Rebuild()方法

LayoutRebuilder涉及到了UGUI的布局系统,我们在这里简单了解一下,元素的自动布局,会在布局系统的文章中介绍。

  • PerformLayoutCalculation()方法会递归计算UI元素的宽高(先计算子元素,在计算自身元素)
    ILayoutElement.CalculateLayoutInputXXXXXX()在具体的实现类中计算该UI的大小
  • PerformLayoutControl()方法会递归设置UI元素的宽高(先设置自身元素,在设置子元素)
    ILayoutController.SetLayoutXXXXX()在具体的实现类中设置该UI的大小
        public void Rebuild(CanvasUpdate executing)
        {
            switch (executing)
            {
                case CanvasUpdate.Layout:
                    // It's unfortunate that we'll perform the same GetComponents querys for the tree 2 times,
                    // but each tree have to be fully iterated before going to the next action,
                    // so reusing the results would entail storing results in a Dictionary or similar,
                    // which is probably a bigger overhead than performing GetComponents multiple times.
                    PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputHorizontal());
                    PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutHorizontal());
                    PerformLayoutCalculation(m_ToRebuild, e => (e as ILayoutElement).CalculateLayoutInputVertical());
                    PerformLayoutControl(m_ToRebuild, e => (e as ILayoutController).SetLayoutVertical());
                    break;
            }
        }
        private void PerformLayoutControl(RectTransform rect, UnityAction action)
        {
            if (rect == null)
                return;

            var components = ListPool.Get();
            rect.GetComponents(typeof(ILayoutController), components);
            StripDisabledBehavioursFromList(components);

            // If there are no controllers on this rect we can skip this entire sub-tree
            // We don't need to consider controllers on children deeper in the sub-tree either,
            // since they will be their own roots.
            if (components.Count > 0)
            {
                // Layout control needs to executed top down with parents being done before their children,
                // because the children rely on the sizes of the parents.

                // First call layout controllers that may change their own RectTransform
                for (int i = 0; i < components.Count; i++)
                    if (components[i] is ILayoutSelfController)
                        action(components[i]);

                // Then call the remaining, such as layout groups that change their children, taking their own RectTransform size into account.
                for (int i = 0; i < components.Count; i++)
                    if (!(components[i] is ILayoutSelfController))
                        action(components[i]);

                for (int i = 0; i < rect.childCount; i++)
                    PerformLayoutControl(rect.GetChild(i) as RectTransform, action);
            }

            ListPool.Release(components);
        }

        private void PerformLayoutCalculation(RectTransform rect, UnityAction action)
        {
            if (rect == null)
                return;

            var components = ListPool.Get();
            rect.GetComponents(typeof(ILayoutElement), components);
            StripDisabledBehavioursFromList(components);

            // If there are no controllers on this rect we can skip this entire sub-tree
            // We don't need to consider controllers on children deeper in the sub-tree either,
            // since they will be their own roots.
            if (components.Count > 0  || rect.GetComponent(typeof(ILayoutGroup)))
            {
                // Layout calculations needs to executed bottom up with children being done before their parents,
                // because the parent calculated sizes rely on the sizes of the children.

                for (int i = 0; i < rect.childCount; i++)
                    PerformLayoutCalculation(rect.GetChild(i) as RectTransform, action);

                for (int i = 0; i < components.Count; i++)
                    action(components[i]);
            }

            ListPool.Release(components);
        }

6.0 Rebuild()元素加入“待重建队列”

我们回到Canvas.willRenderCanvases事件,当Mesh需要重建时,Unity底层会自动调用,那么如果某个UI需要重建,我们只需要将它加入到“待重建队列”中,等待下一次Untiy系统回调Canvas.willRenderCanvases事件时,一起Rebuild即可。那么UI元素什么情况下会添加到“待重建队列”中的呢?
由于元素对的改变分为布局变化,顶点变化,材质变化,所以分别提供了三个方法SetLayoutDirty()更新布局,SetVerticesDirty()更新顶点,SetMaterialDirty()更新材质。

Graphic.cs部分源码如下:

        /// 
        /// Set all properties of the Graphic dirty and needing rebuilt.
        /// Dirties Layout, Vertices, and Materials.
        /// 
        public virtual void SetAllDirty()
        {
            SetLayoutDirty();
            SetVerticesDirty();
            SetMaterialDirty();
        }

        /// 
        /// Mark the layout as dirty and needing rebuilt.
        /// 
        /// 
        /// Send a OnDirtyLayoutCallback notification if any elements are registered. See RegisterDirtyLayoutCallback
        /// 
        public virtual void SetLayoutDirty()
        {
            if (!IsActive())
                return;

            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

            if (m_OnDirtyLayoutCallback != null)
                m_OnDirtyLayoutCallback();
        }

        /// 
        /// Mark the vertices as dirty and needing rebuilt.
        /// 
        /// 
        /// Send a OnDirtyVertsCallback notification if any elements are registered. See RegisterDirtyVerticesCallback
        /// 
        public virtual void SetVerticesDirty()
        {
            if (!IsActive())
                return;

            m_VertsDirty = true;
            CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

            if (m_OnDirtyVertsCallback != null)
                m_OnDirtyVertsCallback();
        }
        /// 
        /// Mark the material as dirty and needing rebuilt.
        /// 
        /// 
        /// Send a OnDirtyMaterialCallback notification if any elements are registered. See RegisterDirtyMaterialCallback
        /// 
        public virtual void SetMaterialDirty()
        {
            if (!IsActive())
                return;

            m_MaterialDirty = true;
            CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

            if (m_OnDirtyMaterialCallback != null)
                m_OnDirtyMaterialCallback();
        }

6.1 SetVerticesDirty(),SetMaterialDirty()

可以发现“待重建队列(m_GraphicRebuildQueue)”的待重建元素通过CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this)方法添加。

CanvasUpdateRegistry.cs部分源码如下:

        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);
        }

6.2 SetLayoutDirty()

“待重建队列(m_LayoutRebuildQueue)”的待重建元素通过LayoutRebuilder.MarkLayoutForRebuild(rectTransform)方法,进而通过CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(rebuilder)方法添加。

LayoutRebuilder.cs部分源码如下:

        /// 
        /// Mark the given RectTransform as needing it's layout to be recalculated during the next layout pass.
        /// 
        /// Rect to rebuild.
        public static void MarkLayoutForRebuild(RectTransform rect)
        {
            if (rect == null || rect.gameObject == null)
                return;

            var comps = ListPool.Get();
            bool validLayoutGroup = true;
            RectTransform layoutRoot = rect;
            var parent = layoutRoot.parent as RectTransform;
            while (validLayoutGroup && !(parent == null || parent.gameObject == null))
            {
                validLayoutGroup = false;
                parent.GetComponents(typeof(ILayoutGroup), comps);

                for (int i = 0; i < comps.Count; ++i)
                {
                    var cur = comps[i];
                    if (cur != null && cur is Behaviour && ((Behaviour)cur).isActiveAndEnabled)
                    {
                        validLayoutGroup = true;
                        layoutRoot = parent;
                        break;
                    }
                }

                parent = parent.parent as RectTransform;
            }

            // We know the layout root is valid if it's not the same as the rect,
            // since we checked that above. But if they're the same we still need to check.
            if (layoutRoot == rect && !ValidController(layoutRoot, comps))
            {
                ListPool.Release(comps);
                return;
            }

            MarkLayoutRootForRebuild(layoutRoot);
            ListPool.Release(comps);
        }
        private static void MarkLayoutRootForRebuild(RectTransform controller)
        {
            if (controller == null)
                return;

            var rebuilder = s_Rebuilders.Get();
            rebuilder.Initialize(controller);
            if (!CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(rebuilder))
                s_Rebuilders.Release(rebuilder);
        }

CanvasUpdateRegistry.cs部分源码如下:

        /// 
        /// Try and add the given element to the layout rebuild list.
        /// 
        /// The element that is needing rebuilt.
        /// 
        /// True if the element was successfully added to the rebuilt list.
        /// False if either already inside a Graphic Update loop OR has already been added to the list.
        /// 
        public static bool TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
        {
            return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
        }

        private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
        {
            if (m_LayoutRebuildQueue.Contains(element))
                return false;

            /* TODO: this likely should be here but causes the error to show just resizing the game view (case 739376)
            if (m_PerformingLayoutUpdate)
            {
                Debug.LogError(string.Format("Trying to add {0} for layout rebuild while we are already inside a layout rebuild loop. This is not supported.", element));
                return false;
            }*/

            return m_LayoutRebuildQueue.AddUnique(element);
        }

6.3 方法调用

为什么UI发生变化一定要加入“待重建队列”中呢?其实这个不难想象,一个UI界面同一帧可能有N个对象发生变化,任意一个变化都需要重建UI那么肯定会卡死,所以我们先把需要重建的UI加入到队列中,等待一个统一的时机来合并。

我们已经知道了元素是如何加入到“待重建队列”中的,那么我们只需要看下对应的方法是在哪里调用的就可以啦,调用的地方较多,请自行通过IDE查看SetAllDirty(),SetLayoutDirty(),SetVerticesDirty(),SetMaterialDirty()方法的引用信息。这里简单举几个例子:
Graphic.cs部分源码如下:

  • RectTransform的Anchor,Width,Height,Anchor,Pivot改变时调用,注意改变Position,Rotation,Scale不会调用。
        protected override void OnRectTransformDimensionsChange()
        {
            if (gameObject.activeInHierarchy)
            {
                // prevent double dirtying...
                if (CanvasUpdateRegistry.IsRebuildingLayout())
                    SetVerticesDirty();
                else
                {
                    SetVerticesDirty();
                    SetLayoutDirty();
                }
            }
        }
  • 父物体改变时调用
        protected override void OnTransformParentChanged()
        {
            base.OnTransformParentChanged();

            m_Canvas = null;

            if (!IsActive())
                return;

            CacheCanvas();
            GraphicRegistry.RegisterGraphicForCanvas(canvas, this);
            SetAllDirty();
        }
  • Material改变时调用
        /// 
        /// The Material set by the user
        /// 
        public virtual Material material
        {
            get
            {
                return (m_Material != null) ? m_Material : defaultMaterial;
            }
            set
            {
                if (m_Material == value)
                    return;

                m_Material = value;
                SetMaterialDirty();
            }
        }

UI的网格我们都已经合并到了同一个Mesh中,这时我们只需要保证贴图(使用Atals图集),材质,Shader相同就可以真正合并成一个DrawCall了。

你可能感兴趣的:(UGUI笔记——UI Mesh Rebuild)