KaaPexei 发表于 2022-6-6 07:13

【Unity源码学习】网格重建

前言

了解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、scaleSetVerticesDirty

拿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;
                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;
                  if (ObjectValidForUpdate(element))
                        element.Rebuild((CanvasUpdate)i);//重建UI元素
                }
                catch (Exception e)
                {
                  Debug.LogException(e, instance.m_GraphicRebuildQueue.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);
      }

franciscochonge 发表于 2022-6-6 07:18

nice

JamesB 发表于 2022-6-6 07:18

修改RectTransform的Scale也会导致Rebuild吗?

acecase 发表于 2022-6-6 07:25

是的

DomDomm 发表于 2022-6-6 07:29

对于RectTransform的属性进行修改m_LayoutRebuildQueue始终没有注册
并且修改Scale 和 position 不会进行Rebuild

BlaXuan 发表于 2022-6-6 07:34

请问什么时候才会引起m_LayoutRebuildQueue的变化,您现在找到了吗
页: [1]
查看完整版本: 【Unity源码学习】网格重建