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

目录

4.4.1 UGUI 核心源码结构

4.4.2 Culling 模块

4.4.3 Layout 布局模块

CanvasScaler的核心函数

4.4.4 MaterialModifiers、SpecializedCollections、Utility

4.4.5 VertexModifiers

4.4.6 核心渲染类

RegisterCanvasElementForGraphicRebuild()

重构时的逻辑

执行网格构建函数

 Mask 遮罩部分

核心部分

RectMask2D

核心部分源码

SetClipRect  


4.4.1 UGUI 核心源码结构

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

4.4.2 Culling 模块

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

        

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的核心函数

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, 它们的共同关键代码如下:

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 相对比较重要,它是基础类,也存放了核心算法。

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()

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

重构时的逻辑

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 函数进行重构,最后做一些清理的事务。 

执行网格构建函数

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 容器对象池,将所有使用过的废弃的数据都存储在里对象池的容器中,当需要时再拿旧的继续使用。

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() 函数。 

    protected override void OnPopulateMesh(VertexHelper toFill)

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

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

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

 Mask 遮罩部分

核心部分

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

核心部分源码

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 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。

    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  

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

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

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

  • 13
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值