当前位置: 首页 > article >正文

Unity UGUI的核心渲染组件

目录

Graphic

Mask

RectMask2D

Graphic

在UGUI中常用的组件有Image、RawImage、Mask、RectMask2D、Text、InputField中,Image、RawImage、Text都继承自MaskableGraphic, MaskableGraphic 又继承自Graphic。所以Graphic是一个非常重要的类。让我们来对着Graphic的源码分析用作原理。

/// <summary>
/// Set all properties of the Graphic dirty and needing rebuilt.
/// Dirties Layout, Vertices, and Materials.
/// </summary>
public virtual void SetAllDirty()
{
    // Optimization: Graphic layout doesn't need recalculation if
    // the underlying Sprite is the same size with the same texture.
    // (e.g. Sprite sheet texture animation)

    if (m_SkipLayoutUpdate)
    {
        m_SkipLayoutUpdate = false;
    }
    else
    {
        SetLayoutDirty();
    }

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

    SetVerticesDirty();
}

/// <summary>
/// Mark the layout as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyLayoutCallback notification if any elements are registered. See RegisterDirtyLayoutCallback
/// </remarks>
public virtual void SetLayoutDirty()
{
    if (!IsActive())
        return;

    LayoutRebuilder.MarkLayoutForRebuild(rectTransform);

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

/// <summary>
/// Mark the vertices as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyVertsCallback notification if any elements are registered. See RegisterDirtyVerticesCallback
/// </remarks>
public virtual void SetVerticesDirty()
{
    if (!IsActive())
        return;

    m_VertsDirty = true;
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

/// <summary>
/// Mark the material as dirty and needing rebuilt.
/// </summary>
/// <remarks>
/// Send a OnDirtyMaterialCallback notification if any elements are registered. See RegisterDirtyMaterialCallback
/// </remarks>
public virtual void SetMaterialDirty()
{
    if (!IsActive())
        return;

    m_MaterialDirty = true;
    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);

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

SetAllDirty()方法将设置并通知元素重新布局、重新构建网格及材质球。该方法通知LayoutRebuilder布局管理类进行重新布局,在LayoutRebuilder.MarkLayout-ForRebuild()中,它调用CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild()加入重构队伍,最终重构布局。

SetLayoutDirty()、SetVerticesDirty()、SetMaterialDirty()都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(),被调用时可以认为是通知它去重构网格,但它并没有立即重新构建。这边代码使用的也是标脏模式,这里就不详细解释该模式是什么有兴趣的去了解。当标脏后,是将需要重构的元件数据加入IndexedSet容器中,等待下次重构。

注意,CanvasUpdateRegistry只负责重构网格,并不负责渲染和合并。我们来看看CanvasUpdateRegistry的RegisterCanvasElementForGraphicRebuild()函数部分:

/// <summary>
/// Try and add the given element to the rebuild list.
/// Will not return if successfully added.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

/// <summary>
/// Try and add the given element to the rebuild list.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
/// <returns>
/// True if the element was successfully added to the rebuilt list.
/// False if either already inside a Graphic Update loop OR has already been added to the list.
/// </returns>
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);
}

InternalRegisterCanvasElementForGraphicRebuild()将元素放入重构队列中等待下一次重构。重构时的逻辑源码如下:

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

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

我们再来看看Graphic的另一个重要的函数,即执行网格构建函数,代码如下:

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

代码中先调用 OnPopulateMesh() 创建自己的网格,然后调用所有需要需修改网格的网格修饰器(IMeshModifier),通常效果组件(描边等效果组件-我之实现的 Unity Image 镜像 就是实现该接口)进行修改,最后放入CanvasRenderer

这里使用VertexHelper是为了节省内存和CPU,它内部采用List容器对象池,将所有使用过的废弃数据都存储在对象池的容器中。

组件中,Image、RawImage、Text都override(重写)了OnPopulateMesh()函数,这些都需要有自己自定义的网格样式来构建不同类型的画面。其实CanvasRendererCanvas才是合并网格的关键,但CanvasRenderer和Canvas并没有开源出来。

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

虽然拿不到源码但是大致可以猜测这部分,无非就是每次重构时获取Canvas下面所有的CanvasRenderer实例,将它们的网格合并起来,仅此而已。因此关键还是要看如何减少重构次数、提高内存和提高CPU的使用效率。

Mask

Mask的遮罩功能是非常值得我们关注的部分

/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    if (!MaskEnabled())
        return baseMaterial;

    var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
    var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
    if (stencilDepth >= 8)
    {
        Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
        return baseMaterial;
    }

    int desiredStencilBit = 1 << stencilDepth;

    // if we are at the first level...
    // we want to destroy what is there
    if (desiredStencilBit == 1)
    {
        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;
    }

    //otherwise we need to be a bit smarter and set some read / write masks
    var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
    StencilMaterial.Remove(m_MaskMaterial);
    m_MaskMaterial = maskMaterial2;

    graphic.canvasRenderer.hasPopInstruction = true;
    var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
    StencilMaterial.Remove(m_UnmaskMaterial);
    m_UnmaskMaterial = unmaskMaterial2;
    graphic.canvasRenderer.popMaterialCount = 1;
    graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

    return m_MaskMaterial;
}

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

RectMask2D

RectMask2D和Mask一样可以实现遮罩,但是工作原理并不一样让我们来看下RectMask2D的核心代码:

public virtual void PerformClipping()
{
    if (ReferenceEquals(Canvas, null))
    {
        return;
    }

    //TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)

    // 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 the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect
    // overlaps that of the root canvas.
    RenderMode renderMode = Canvas.rootCanvas.renderMode;
    bool maskIsCulled =
        (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
        !clipRect.Overlaps(rootCanvasRect, true);

    if (maskIsCulled)
    {
        // Children are only displayed when inside the mask. If the mask is culled, then the children
        // inside the mask are also culled. In that situation, we pass an invalid rect to allow callees
        // to avoid some processing.
        clipRect = Rect.zero;
        validRect = false;
    }

    if (clipRect != m_LastClipRectCanvasSpace)
    {
        foreach (IClippable clipTarget in m_ClipTargets)
        {
            clipTarget.SetClipRect(clipRect, validRect);
        }

        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            maskableTarget.SetClipRect(clipRect, validRect);
            maskableTarget.Cull(clipRect, validRect);
        }
    }
    else if (m_ForceClip)
    {
        foreach (IClippable clipTarget in m_ClipTargets)
        {
            clipTarget.SetClipRect(clipRect, validRect);
        }

        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            maskableTarget.SetClipRect(clipRect, validRect);

            if (maskableTarget.canvasRenderer.hasMoved)
                maskableTarget.Cull(clipRect, validRect);
        }
    }
    else
    {
        foreach (MaskableGraphic maskableTarget in m_MaskableTargets)
        {
            //Case 1170399 - hasMoved is not a valid check when animating on pivot of the object
            maskableTarget.Cull(clipRect, validRect);
        }
    }

    m_LastClipRectCanvasSpace = clipRect;
    m_ForceClip = false;

    UpdateClipSoftness();
}

从代码中可以看出ReckMask2D会先计算并设置clipRect裁剪范围,在对所有子节点设置裁剪操作。

使用

MaskUtilities.GetRectMasksForClip(this, m_Clippers);

来获取所有有关联的RectMask2D 范围,然后由

Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);

获得所有需要裁剪的对象,实际上是计算出不需要裁剪的部分,剩下的都进行最后的裁剪:

foreach (IClippable clipTarget in m_ClipTargets)
{
    clipTarget.SetClipRect(clipRect, validRect);
}

对所有需要裁剪的UI元素进行裁剪操作。其中SetClipRect裁剪操作的源码如下:

/// <summary>
/// See IClippable.SetClipRect
/// </summary>
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
    if (validRect)
        canvasRenderer.EnableRectClipping(clipRect);
    else
        canvasRenderer.DisableRectClipping();
}

最后的操作是在CanvasRenderer中进行的,前面我们说CanvasRenderer的看不了源码。但可以很容易想到这里面的操作是什么,即计算两个四边形的相交点,再组合成裁剪后的内容。至此我们对UGUI的核心渲染流程有了一定的认识。其实并没有高深的算法或者技术,所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁剪来进行的。很多性能的关键在于,如何减少重构次数,以及提高内存和CPU的使用效率。


http://www.kler.cn/a/315717.html

相关文章:

  • 免费,WPS Office教育考试专用版
  • 【前端】Vue中如何避免出现内存泄漏
  • leetcode hot100【LeetCode 114.二叉树展开为链表】java实现
  • Web安全之SQL注入---基础
  • 【学习笔记】数据结构(七)
  • Linux探秘坊-------1.系统核心的低语:基础指令的奥秘解析(1)
  • FFmpeg中结构释放小函数
  • Python在数据科学与机器学习中的应用
  • C语言 | Leetcode C语言题解之第429题N叉树的层序遍历
  • Nginx简介;Nginx安装
  • Chainlit集成LlamaIndex实现知识库高级检索(自动合并检索)
  • VUE3学习---【一】【从零开始的VUE学习】
  • Java面试篇基础部分-Synchronized关键字详解
  • python爬虫中json和xml字符串的xPath和jsonpath过滤语法区别对比
  • 零工市场小程序:推动零工市场建设
  • 【Kubernetes】常见面试题汇总(三十)
  • 【二等奖论文】2024年华为杯研赛D题成品论文(后续会更新)
  • rust GTK4 窗口创建与 wayland Subsurface (vulkan 渲染窗口初始化 (Linux) 上篇)
  • Docker实践——天池篇
  • 极度精简 Winows11 系统镜像!Tiny11 2311下载 - 支持苹果 M 芯片 Mac 安装 (ARM 精简版)!
  • get_property --Cmakelist之中
  • 关闭小广告【JavaScript】
  • 【线程】线程的同步
  • PHP转Go很丝滑开发框架设计思路-把php优秀设计借鉴到Go框架设计里面-保留php开发习惯又能提供高软件性能
  • OpenCV特征检测(8)检测图像中圆形的函数HoughCircles()的使用
  • 利用JAVA写一张纸折叠珠穆拉玛峰高度