UGUI核心源码剖析

前言

Core模块主要为Culling(裁剪)、Layout(布局)、MaterialModifiers(材质球修改器)、SpecializedCollection(收集),Utility(实用工具)、vc(顶点修改器)。

一、Culling裁剪模块

Culling是对模型裁剪的工具类,大都用到Mask遮罩上,只要Mask才有裁剪的需求。Cliping类FindCullAndClipWorldRect就是将很多 RectMask2D 重叠部分,计算出它们的重叠部分的区域。

        public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
        {
            if (rectMaskParents.Count == 0)
            {
                validRect = false;
                return new Rect();
            }

            Rect current = rectMaskParents[0].canvasRect;
            Vector4 offset = rectMaskParents[0].padding;
            float xMin = current.xMin + offset.x;
            float xMax = current.xMax - offset.z;
            float yMin = current.yMin + offset.y;
            float yMax = current.yMax - offset.w;

            var rectMaskParentsCount = rectMaskParents.Count;
            for (var i = 1; i < rectMaskParentsCount; ++i)
            {
                current = rectMaskParents[i].canvasRect;
                offset = rectMaskParents[i].padding;
                if (xMin < current.xMin + offset.x)
                    xMin = current.xMin + offset.x;
                if (yMin < current.yMin + offset.y)
                    yMin = current.yMin + offset.y;
                if (xMax > current.xMax - offset.z)
                    xMax = current.xMax - offset.z;
                if (yMax > current.yMax - offset.w)
                    yMax = current.yMax - offset.w;
            }

            validRect = xMax > xMin && yMax > yMin;
            return validRect ? new Rect(xMin, yMin, xMax - xMin, yMax - yMin) : new Rect();
        }

二、Layout布局模块

Layout主要功能是布局方面的,包括横向布局,纵向布局,方格布局等。

除了布局内容之外,其余还有3个文件,CanvasScalerAspectRatioFitterContentSizeFitter是调整自适应功能。

ContentSizeFitter处理的是内容自适应,AspectRatioFitter是朝向自适应,其中包括以长度为基准,以宽度为基准,以父节点为基准,以外层父节点为基准的四种自适应方式。

CanvasScaler操作的是Canvas整个画布针对不同的屏幕进行自适应调整。一共有三种ScreenMathMode模式,不同模式对应屏幕不同的适应算法。包括优先匹配长或宽最小化固定拉伸最大化固定拉伸

        // 处理不同的ScaleMode
        protected virtual void Handle()
        {
            if (m_Canvas == null || !m_Canvas.isRootCanvas)
                return;

            if (m_Canvas.renderMode == RenderMode.WorldSpace)
            {
                HandleWorldCanvas();
                return;
            }

            switch (m_UiScaleMode)
            {
                case ScaleMode.ConstantPixelSize: HandleConstantPixelSize(); break;
                case ScaleMode.ScaleWithScreenSize: HandleScaleWithScreenSize(); break;
                case ScaleMode.ConstantPhysicalSize: HandleConstantPhysicalSize(); break;
            }
        }

       // 处理缩放根据ScreenSize
        protected virtual void HandleScaleWithScreenSize()
        {
            Vector2 screenSize = m_Canvas.renderingDisplaySize;

            // Multiple display support only when not the main display. For display 0 the reported
            // resolution is always the desktops resolution since its part of the display API,
            // so we use the standard none multiple display method. (case 741751)
            int displayIndex = m_Canvas.targetDisplay;
            if (displayIndex > 0 && displayIndex < Display.displays.Length)
            {
                Display disp = Display.displays[displayIndex];
                screenSize = new Vector2(disp.renderingWidth, disp.renderingHeight);
            }

            float scaleFactor = 0;
            switch (m_ScreenMatchMode)
            {
                case ScreenMatchMode.MatchWidthOrHeight:
                {
                    // We take the log of the relative width and height before taking the average.
                    // Then we transform it back in the original space.
                    // the reason to transform in and out of logarithmic space is to have better behavior.
                    // If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
                    // In normal space the average would be (0.5 + 2) / 2 = 1.25
                    // In logarithmic space the average is (-1 + 1) / 2 = 0
                    float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);
                    float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);
                    float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);
                    scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);
                    break;
                }
                case ScreenMatchMode.Expand:
                {
                    scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
                    break;
                }
                case ScreenMatchMode.Shrink:
                {
                    scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);
                    break;
                }
            }

            SetScaleFactor(scaleFactor);
            SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit);
        }

三、VertexModifiers顶点修改器类

VertexModifiers主要用于修改图形网格,尤其是在UI元素网格生成完毕后进行二次修改。

其中BaseMeshEffect是抽象基类, 提供所有在修改UI元素网格时所需要的变量和接口。

IMeshModifier是关键接口,渲染核心类Graphic中会获取所有拥有这个接口的组件,然后依次遍历并调用ModifyMesh接口来触发改变图像网格的效果。

Outline(包边框),Shadow(阴影),PositionAsUV1(位置UV) 都继承了 BaseMeshEffect 基类,并实现了关键接口 ModifyMesh。其中 Outline 继承自 Shadow。

核心代码:在原有的Mesh顶点基础上,加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得原图形外渲染出外描边或者阴影

        protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
        {
            UIVertex vt;

            var neededCapacity = verts.Count + end - start;
            if (verts.Capacity < neededCapacity)
                verts.Capacity = neededCapacity;

            for (int i = start; i < end; ++i)
            {
                vt = verts[i];
                verts.Add(vt);

                Vector3 v = vt.position;
                v.x += x;
                v.y += y;
                vt.position = v;
                var newColor = color;
                if (m_UseGraphicAlpha)
                    newColor.a = (byte)((newColor.a * verts[i].color.a) / 255);
                vt.color = newColor;
                verts[i] = vt;
            }
        }

四、工具类

MaterialModifiers(材质球修改器), SpecializedCollections(特殊收集器), Utility(使用工具)是其他模块所依赖的工具。

IMaterialModifier 是一个接口类,为Mask 遮罩修改材质球所准备的,但所用方法都需要各自实现

IndexedSet 是一个容器,在很多核心代码上都有使用,它加速了移除元素的速度,以及加速了元素包含判断。

ListPoolList容器对象池,ObjectPool是普通对象池,很多代码上都用到了它们,对象池让内存利用率更高。

VertexHelper 特别重要,它是用来存储生成 Mesh 网格需要的所有数据,由于在Mesh生成的过程中顶点的生成频率非常高,因此 VertexHelper 存储了 Mesh 的所有相关数据的同时,用上面提到的ListPoolObjectPool做为对象池来生成和销毁,使得数据高效得被重复利用,不过它并不负责计算和生成 Mesh,计算和生成由各自图形组件来完成,它只为它们提供计算后的数据存储服务。

五、核心渲染类

我们常用的Image、RawImage、Mask、RectMask2D、Text、InputField中,Image,RawImage,Text都是继承了MaskableGraphic,而MaskableGraphic又继承自Graphic类。除了这几个类外, CanvasUpdateRegistry是存储和管理所有课绘制元素大的管理类。其中Graphic类非常重要,是基础类也存在核心算法。

5.1 Graphic.SetAllDirty()

  • SetLayoutDirty: 设置布局脏数据
  • SetMaterialDirty: 设置材质球脏数据
  • SetVerticesDirty:设置顶点脏数据
  • SetRaycastDirty:设置射线脏数据

SetLayoutDirtySetMaterialDirtySetVerticesDirty都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild,它被调用时可以认为是通知它去重新重构Mesh,但它并没有立即重新构建,而是将需要重构的元件数据加入到IndexedSet容器中,等待下次重构。

        public virtual void SetAllDirty()
        {
            if (m_SkipLayoutUpdate)
            {
                m_SkipLayoutUpdate = false;
            }
            else
            {
                SetLayoutDirty();
            }

            if (m_SkipMaterialUpdate)
            {
                m_SkipMaterialUpdate = false;
            }
            else
            {
                SetMaterialDirty();
            }

            SetVerticesDirty();
            SetRaycastDirty();
        }

    // 设置布局脏数据
        public virtual void SetLayoutDirty()
        {
            if (!IsActive())
                return;

            LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

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

    // 设置材质球脏数据
        public virtual void SetMaterialDirty()
        {
            if (!IsActive())
                return;

            m_MaterialDirty = true;
            CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

    // 设置顶点脏数据 
    public virtual void SetVerticesDirty()
        {
            if (!IsActive())
                return;

            m_VertsDirty = true;
            CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

    // 设置raycast响应事件脏数据
        public void SetRaycastDirty()
        {
            if (m_RaycastTargetCache != m_RaycastTarget)
            {
                if (m_RaycastTarget && isActiveAndEnabled)
                    GraphicRegistry.RegisterRaycastGraphicForCanvas(canvas, this);

                else if (!m_RaycastTarget)
                    GraphicRegistry.UnregisterRaycastGraphicForCanvas(canvas, this);
            }
            m_RaycastTargetCache = m_RaycastTarget;
        }

5.2 CanvasUpdateRegistry() 重构Mesh

CanvasUpdateRegistry:负责重构Mesh网格。

        // IndexedSet:重构数据
    private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();        

    public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
        {
            instance.InternalRegisterCanvasElementForGraphicRebuild(element);
        }

    public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
        {
            return 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);
        }

5.3 Graphic.PerformUpdate()

重构调用中的逻辑,先将需要重新布局的元素取出来一个个调用Rebuild函数,在对布局的元素进行裁剪,裁剪后对布局中每个需要重构的元素取出来调用Rebuild函数进行重构。

        private static readonly Comparison<ICanvasElement> 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++)
            {
                UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);

                for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
                {
                    var rebuild = m_LayoutRebuildQueue[j];
                    try
                    {
                        if (ObjectValidForUpdate(rebuild))
                            rebuild.Rebuild((CanvasUpdate)i);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e, rebuild.transform);
                    }
                }
                UnityEngine.Profiling.Profiler.EndSample();
            }

            for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
                m_LayoutRebuildQueue[i].LayoutComplete();

            m_LayoutRebuildQueue.Clear();
            m_PerformingLayoutUpdate = false;
            UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
            UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);
        // 裁剪
            // now layout is complete do culling...
            UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);
            ClipperRegistry.instance.Cull();
            UnityEngine.Profiling.Profiler.EndSample();

            // 元素重构
            m_PerformingGraphicUpdate = true;
            for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
            {
                UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
                for (var k = 0; k < m_GraphicRebuildQueue.Count; k++)
                {
                    try
                    {
                        var element = m_GraphicRebuildQueue[k];
                        if (ObjectValidForUpdate(element))
                            element.Rebuild((CanvasUpdate)i);
                    }
                    catch (Exception e)
                    {
                        Debug.LogException(e, m_GraphicRebuildQueue[k].transform);
                    }
                }
                UnityEngine.Profiling.Profiler.EndSample();
            }

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

            m_GraphicRebuildQueue.Clear();
            m_PerformingGraphicUpdate = false;
            UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
        }

5.4 Graphic.DoMeshGeneration()

DoMeshGeneration是元素重构Rebuild会调用到的,先调用OnPopulateMesh(pɒpjuleɪt发音:pɒpjuleɪt)创建自己的Mesh网格,然后调用所有需要修改 Mesh 的修改者(IMeshModifier)也就是网格后处理组件(描边等效果组件)进行修改,最后放入 CanvasRenderer

其中 CanvasRenderer 是每个绘制元素都必须有的组件,它是画布与渲染的连接组件,通过 CanvasRenderer 我们才能把网格绘制到 Canvas 画布上去。

Imge,RawImag,Text都override重写OnpopulateMesh函数。

protected override void OnPopulateMesh(VertexHelper toFill)

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<Component>.Get();
    GetComponents(typeof(IMeshModifier), components);

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

    ListPool<Component>.Release(components);

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

六、Mask和RectMask2D

6.1 Mask

Mask 组件调用了模板材质球构建了一个自己的材质球,使用了实时渲染中的模板方法来裁切不需要显示的部分,在 Mask 组件后面的物体都会进行裁切。Mask 是在 GPU 中做的裁切,使用的方法是着色器中的模板方法。

var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;

var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

return m_MaskMaterial;

6.2 RectMask2D

RectMask2D 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。

public virtual void PerformClipping()
{
    if (m_ShouldRecalculateClipRects)
    {
        // 获取了所有有关联的 RectMask2D 遮罩
        MaskUtilities.GetRectMasksForClip(this, m_Clippers);
        m_ShouldRecalculateClipRects = false;
    }

    bool validRect = true;
    // 获取了所有有关联的 RectMask2D 遮罩范围
    Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
    if (clipRect != m_LastClipRectCanvasSpace)
    {
        // 计算了不需要裁切的部分,其他部分都进行裁切
        for (int i = 0; i < m_ClipTargets.Count; ++i)
            m_ClipTargets[i].SetClipRect(clipRect, validRect);

        m_LastClipRectCanvasSpace = clipRect;
        m_LastClipRectValid = validRect;
    }

    for (int i = 0; i < m_ClipTargets.Count; ++i)
        m_ClipTargets[i].Cull(m_LastClipRectCanvasSpace, m_LastClipRectValid);
}

// 裁切源码
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
    if (validRect)
        canvasRenderer.EnableRectClipping(clipRect);
    else
        canvasRenderer.DisableRectClipping();
}