《Unity3D高级编程 主程手记》第四章 用户界面(四) UGUI 核心源码

目录

[4.4.1 UGUI 核心源码结构](#4.4.1 UGUI 核心源码结构)

[4.4.2 Culling 模块](#4.4.2 Culling 模块)

[4.4.3 Layout 布局模块](#4.4.3 Layout 布局模块)

CanvasScaler的核心函数

[4.4.4 MaterialModifiers、SpecializedCollections、Utility](#4.4.4 MaterialModifiers、SpecializedCollections、Utility)

[4.4.5 VertexModifiers](#4.4.5 VertexModifiers)

[4.4.6 核心渲染类](#4.4.6 核心渲染类)

RegisterCanvasElementForGraphicRebuild()

重构时的逻辑

执行网格构建函数

[Mask 遮罩部分](#Mask 遮罩部分)

核心部分

RectMask2D

核心部分源码

SetClipRect


4.4.1 UGUI 核心源码结构

  • Culling 裁剪
  • Layer 布局
  • MaterialModifiers 材质球修改器
  • SpecializedCollections 收集
  • Utility 实用工具
  • Vertexmodifiers 顶点修改器

4.4.2 Culling 模块

Culling 里是对模型裁剪的工具类,大都用在了 Mask (遮罩)上,只有 Mask 才有裁剪的需求。(这里的 Mask 是指 RectMask2D)

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

    var compoundRect = rectMaskParents[0].canvasRect;
    for (var i = 0; i < rectMaskParents.Count; ++i)
        compoundRect = RectIntersect(compoundRect, rectMaskParents[i].canvasRect);

    var cull = compoundRect.width <= 0 || compoundRect.height <= 0;
    if (cull)
    {
        validRect = false;
        return new Rect();
    }

    Vector3 point1 = new Vector3(compoundRect.x, compoundRect.y, 0.0f);
    Vector3 point2 = new Vector3(compoundRect.x + compoundRect.width, compoundRect.y + compoundRect.height, 0.0f);
    validRect = true;
    return new Rect(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y);
}

private static Rect RectIntersect(Rect a, Rect b)
{
    float xMin = Mathf.Max(a.x, b.x);
    float xMax = Mathf.Min(a.x + a.width, b.x + b.width);
    float yMin = Mathf.Max(a.y, b.y);
    float yMax = Mathf.Min(a.y + a.height, b.y + b.height);
    if (xMax >= xMin && yMax >= yMin)
        return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
    return new Rect(0f, 0f, 0f, 0f);
}

上述代码中的函数为 Clipping 类里的函数,第一个函数 FindCullAndClipWorldRect() 的含义是计算 RectMask2D 重叠部分的区域。第二个函数 RectIntersect() 为第一个函数提供了计算服务,其含义是计算两个矩阵的重叠部分。

4.4.3 Layout 布局模块

  • 横向布局
  • 纵向布局
  • 方格布局
  • ContentSizeFitter 内容的自适应
  • AspectRatioFitter 朝向的自适应,包括以长度、宽度、父节点、外层父节点为基准这四种类型的自适应
  • CanvasScaler 操作 Canvas 整个画布针对不同屏幕进行的自适应调整

CanvasScaler的核心函数

cs 复制代码
protected virtual void HandleScaleWithScreenSize()
{
    Vector2 screenSize = new Vector2(Screen.width, Screen.height);

    float scaleFactor = 0;
    switch (m_ScreenMatchMode)
    {
        case ScreenMatchMode.MatchWidthOrHeight:
        {
            //在取平均值之前,我们先取相对宽度和高度的对数
            //然后将其转换到原始空间
            //进出对数空间的原因是具有更好的行为
            //如果一个轴的分辨率是两倍,而另一个轴的分辨率是一半,那么 widthOrHeight 的值为0.5时,它应该平整
            //在正常空间中,平均值为(0.5 + 2) / 2 = 1.25
            //在对数空间中,平均值是(-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);
}

在不同 ScreenMathMode 模式下 ,CanvasScaler 类对屏幕的适应算法包括优先匹配长或宽的、最小化固定拉伸及最大化固定拉伸三种数学计算方式。其中在优先匹配长或宽算法中介绍了使用 Log 和 Pow 来计算缩放比例可以表现的更好。

4.4.4 MaterialModifiers、SpecializedCollections、Utility

  • IMaterialModifier 是一个接口类,是为 Mask 修改材质球所准备的,所用方法需要各自实现。
  • IndexedSet 是一个容器,在很多核心代码上都可使用,它加速了移除元素的速度,并且加快了元素是否包含某个元素的判断操作。
  • ListPool 是 List 容器对象池,ObjectPool 是普通对象池,很多代码上都用到了它们,它们让内存利用率更高。
  • VertexHelper 特别重要,它是用来存储生成网格(Mesh)需要的所有数据。

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

4.4.5 VertexModifiers

  • VertexModifiers 模块的作用是作为顶点修改器。顶点修改器为效果制作提供了更多基础方法和规则。
  • VertexModifiers 模块主要用于修改图形网格 ,在 UI 元素网格生成完毕后可对其进行二次修改
  • 其中 BaseMeshEffect 是抽象基类,提供所有在修改 UI 元素网格时所需的变量和接口。
  • IMeshModifier 是关键接口,在渲染核心类 Graphic 中会获取所有拥有这个接口的组件,然后依次遍历并调用 ModifyMesh 接口来触发改变图像网格的效果

当前在源码中拥有的二次效果包括 Outline(包边框)、Shadow(阴影)、PositionAsUV1(位置UV),都继承自 BaseMeshEffect 基类,并实现了关键接口 ModifyMesh。其中 Outline 继承自 Shadow, 它们的共同关键代码如下:

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

    var neededCpacity = verts.Count * 2;
    if (verts.Capacity < neededCpacity)
        verts.Capacity = neededCpacity;

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

ApplyShadowZeroAlloc() 的作用是在原有的网格顶点基础上加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得在原图形外渲染出外描边或者阴影。

4.4.6 核心渲染类

我们常用的组件 Image、RawImage、Mask、RectMask2D、Text、InputField 中,Image、RawImage、Text 都是继承自 MaskableGraphic ,而 MaskableGraphic 又继承自 Graphic 类,因此 Graphic 相对比较重要,它是基础类,也存放了核心算法。

cs 复制代码
public virtual void SetAllDirty()
{
    SetLayoutDirty();    //布局需重构
    SetVerticesDirty();  //顶点需重构  
    SetMaterialDirty();  //材质球需重构
}

public virtual void SetLayoutDirty()
{
    if (!IsActive())     //是否激活
        return;            

    LayoutRebuilder.MarkLayoutForRebuild(rectTransform); //标记重构节点
    if (m_OnDirtyLayoutCallback != null) //重构标记回调通知
        m_OnDirtyLayoutCallback();                                    
}

public virtual void SetVerticesDirty()
{
    if (!IsActive())     //是否激活
        return;

    m_VertsDirty = true; //设置重构标记 
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); //将自己注册到重构队列中   

    if (m_OnDirtyVertsCallback != null) //回调通知
        m_OnDirtyVertsCallback();
}

public virtual void SetMaterialDirty()
{
    if (!IsActive())       //是否激活
        return;    

    m_MaterialDirty = true;//标记重构
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this); //将自己注册到重构队列中

    if (m_OnDirtyMaterialCallback != null) //回调通知   
        m_OnDirtyMaterialCallback();
}

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

注意:CanvasUpdateRegistry 只负责重构网格,并不负责渲染和合并

RegisterCanvasElementForGraphicRebuild()

cs 复制代码
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;
    }

    if (m_GraphicRebuildQueue.Contains(element))
        return false;

    m_GraphicRebuildQueue.Add(element);
    return true;
}

重构时的逻辑

cs 复制代码
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;
private void PerformUpdate()
{
    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].LayoutComplete();

    instance.m_GraphicRebuildQueue.Clear();
    m_PerformingGraphicUpdate = false;
}

PerformUpdate 为 CanvasUpdateRegistry 在重构调用中的逻辑。先将需要重新布局的元素取出来,一个个调用 Rebuild 函数重构 ,再对布局后的元素进行裁剪 ,裁剪后对布局中每个需要重构的元素取出来调用 Rebuild 函数 进行重构,最后做一些清理的事务。

执行网格构建函数

cs 复制代码
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);
}

此段代码是 Graphic 构建网格的部分,先调用 OnPopulateMesh创建自己的网格,然后调用所有需要修改网格的修改者(IMeshModifier),也就是效果组件(描边等效果组件)进行修改,最后放入 CanvasRenderer 。

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

这里使用 VertexHelper 是为了节省内存和 CPU 消耗,它内部采用 List 容器对象池,将所有使用过的废弃的数据都存储在里对象池的容器中,当需要时再拿旧的继续使用。

cs 复制代码
public class VertexHelper : IDisposable
{
    private List<Vector3> m_Positions = ListPool<Vector3>.Get();
    private List<Color32> m_Colors = ListPool<Color32>.Get();
    private List<Vector2> m_Uv0S = ListPool<Vector2>.Get();
    private List<Vector2> m_Uv1S = ListPool<Vector2>.Get();
    private List<Vector3> m_Normals = ListPool<Vector3>.Get();
    private List<Vector4> m_Tangents = ListPool<Vector4>.Get();
    private List<int> m_Indicies = ListPool<int>.Get();
}

组件中,Image、RawImage、Text 都 override(重写)了 OnPopulateMesh() 函数。

cs 复制代码
    protected override void OnPopulateMesh(VertexHelper toFill)

其实 CanvasRenderer 和 Canvas 才是合并网格的关键,但 CanvasRenderer 和 Canvas 并没有开源出来。

推测:合并部分是每次重构时获取 Canvas 下面所有的 CanvasRenderer 实例,将它们的网格合并起来。

关键还是要看如何减少重构次数、提高内存和提高 CPU 的使用效率

Mask 遮罩部分

核心部分

cs 复制代码
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;

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

RectMask2D

核心部分源码

cs 复制代码
public virtual void PerformClipping()
{
    // if the parents are changed
    // or something similar we
    // do a recalculate here
    if (m_ShouldRecalculateClipRects)
    {
        MaskUtilities.GetRectMasksForClip(this, m_Clippers);
        m_ShouldRecalculateClipRects = false;
    }

    // get the compound rects from
    // the clippers that are valid
    bool validRect = true;
    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);
}

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

cs 复制代码
    MaskUtilities.GetRectMasksForClip(this, m_Clippers);
    //获取了所有有关联的 RectMask2D 遮罩范围

    Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
    //计算了需要裁切的部分,实际上是计算了不需要裁切的部分,其他部分都进行裁切。

    for (int i = 0; i < m_ClipTargets.Count; ++i)
            m_ClipTargets[i].SetClipRect(clipRect, validRect);
    //对所有需要裁切的UI元素,进行裁切操作。

SetClipRect

cs 复制代码
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
    if (validRect)
        canvasRenderer.EnableRectClipping(clipRect);
    else
        canvasRenderer.DisableRectClipping();
}

最后操作是在 CanvasRenderer 中进行的。推测:计算两个四边形的相交点,再组合成裁切后的内容。

所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁切来进行的。很多性能关键在于,如何减少重构次数,以及提高内存和 CPU 的使用效率。

相关推荐
密码小丑3 分钟前
11月4日(内网横向移动(一))
笔记
鸭鸭梨吖1 小时前
产品经理笔记
笔记·产品经理
Envyᥫᩣ1 小时前
C#语言:从入门到精通
开发语言·c#
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
丫头,冲鸭!!!2 小时前
B树(B-Tree)和B+树(B+ Tree)
笔记·算法
听忆.2 小时前
手机屏幕上进行OCR识别方案
笔记
MediaTea2 小时前
七次课掌握 Photoshop:选区与抠图
ui·photoshop
Selina K3 小时前
shell脚本知识点记录
笔记·shell
3 小时前
开源竞争-数据驱动成长-11/05-大专生的思考
人工智能·笔记·学习·算法·机器学习
霍格沃兹测试开发学社测试人社区4 小时前
软件测试学习笔记丨Flask操作数据库-数据库和表的管理
软件测试·笔记·测试开发·学习·flask