找回密码
 立即注册
查看: 458|回复: 5

[笔记] 【Unity源码学习】网格重建

[复制链接]
发表于 2022-6-6 07:13 | 显示全部楼层 |阅读模式
前言

了解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);
        }
发表于 2022-6-6 07:18 | 显示全部楼层
nice
发表于 2022-6-6 07:18 | 显示全部楼层
修改RectTransform的Scale也会导致Rebuild吗?
发表于 2022-6-6 07:25 | 显示全部楼层
是的
发表于 2022-6-6 07:29 | 显示全部楼层
对于RectTransform的属性进行修改  m_LayoutRebuildQueue始终没有注册
并且修改Scale 和 position 不会进行Rebuild
发表于 2022-6-6 07:34 | 显示全部楼层
请问什么时候才会引起m_LayoutRebuildQueue的变化,您现在找到了吗
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-21 21:26 , Processed in 0.118988 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表