找回密码
 立即注册
查看: 273|回复: 0

[UGUI源码二]Unity UI重建(Rebuild)源码分析

[复制链接]
发表于 2021-12-29 18:24 | 显示全部楼层 |阅读模式
Unity怎么绘制UI元素的?

首先我们需要明白一个问题:Unity是怎么绘制UI元素的?
Unity中渲染的物体都是由网格(Mesh)构成的,而网格的绘制单元是图元(点、线、三角面)。unity中添加一个ImageText,并且将Shadings Mode设置为Wireframe模式,可以看到一个Image由四个顶点和两个三角面构成,Text也是由许多顶点和三角面构成。


绘制信息都存储在Vertexhelper类中,除了顶点外,还包括法线、UV、颜色、切线以及一些函数,下面是它的部分代码
public class VertexHelper : IDisposable
{
     private List<Vector3> m_Positions;
     private List<Color32> m_Colors;
     private List<Vector4> m_Uv0S;
     private List<Vector4> m_Uv1S;
     private List<Vector4> m_Uv2S;
     private List<Vector4> m_Uv3S;
     private List<Vector3> m_Normals;
     private List<Vector4> m_Tangents;
     private List<int> m_Indices;

     public void FillMesh(Mesh mesh)
     {
         InitializeListIfRequired();

         mesh.Clear();

         if (m_Positions.Count >= 65000)
             throw new ArgumentException("Mesh can not have more than 65000 vertices");

         mesh.SetVertices(m_Positions);
         mesh.SetColors(m_Colors);
         mesh.SetUVs(0, m_Uv0S);
         mesh.SetUVs(1, m_Uv1S);
         mesh.SetUVs(2, m_Uv2S);
         mesh.SetUVs(3, m_Uv3S);
         mesh.SetNormals(m_Normals);
         mesh.SetTangents(m_Tangents);
         mesh.SetTriangles(m_Indices, 0);
         mesh.RecalculateBounds();
     }
}
数据存储好了,那怎么绘制呢?
这是依靠CanvasRenderer来完成的,它听起来可能比较陌生,但实际上当我们在项目中创建的一些UI元素,比如ButtonImageText时,都包含组件CanvasRenderer,这个类提供了许多关键绘制信息,比如被渲染物体的颜色、材质和Mesh等,主要作用就是渲染包含在Canvas中的UI对象,但是在Inspector界面中并不会展示任何属性。



下面列出了几个比较重要的属性和方法,详情见Unity Documentation: CanvasRenderer。


下面对这些步骤展开详细的讲解。
ICanvasElement

首先是ICanvasElement接口,重建的时候会调用它的Rebuild方法,继承它的类都会对这个函数进行重写,Unity中几乎所有的UI组件都继承自这个接口。


下面是接口中包含的方法。
public interface ICanvasElement
{
    // 根据CanvasUpdate的不同阶段重建元素
    void Rebuild(CanvasUpdate executing);

    // 获取ICanvasElement关联的变换组件
    Transform transform { get; }

    // 布局重建完成的回调函数
    void LayoutComplete();

    // 图形重建完成的回调函数
    void GraphicUpdateComplete();

    // 是否被销毁
    bool IsDestroyed();
}
可以看到,Rebuild函数需要提供CanvasUpdate类型的参数,它是一个枚举类型,表示Rebuild的不同阶段。
public enum CanvasUpdate
{
    //布局重建前
    Prelayout = 0,
    //布局重建
    Layout = 1,
    //布局重建后
    PostLayout = 2,
    //渲染前(图形重建前)
    PreRender = 3,
    //PreRender后,渲染前
    LatePreRender = 4,
    //最大枚举值
    MaxUpdateValue = 5
}
CanvasUpdateRegistry

一个继承自ICanvasElement接口的类如果要重建,需要将自身加入到CanvasUpdateRegistry类中的重建队列中(RebuildQueue,并不是数据结构中的队列),CanvasUpdateRegistry中包含两个索引集(IndexedSet,内部使用DictionaryList存储数据),分别是

  • IndexedSet<ICanvasElement> m_LayoutRebuildQueue ,布局重建队列,当UI元素的布局需要更新时将其加入队列
  • IndexedSet<ICanvasElement> m_GraphicRebuildQueue ,图形重建队列,当UI元素的图像需要更新时将其加入队列
该类在构造函数中监听了CanvaswillRenderCanvases事件,这个事件会在渲染前进行每帧调用,函数大致包含以下步骤:

  • PerformUpdate函数对m_LayoutRebuildQueue中的元素进行排序,依据是父节点的多少。接下来依次将PrelayoutLayoutPostLayout作为参数传递给Rebuild进行布局重建,完成后通知布局队列中的元素重建完成。
  • 调用ClipperRegistryCull函数进行裁剪。
  • 进行图形重建,遍历m_GraphicRebuildQueue的值,分别将参数PreRenderLatePreRender作为参数传递给Rebuild函数进行图形重建。
  • 最后通知图形重建完成。
protected CanvasUpdateRegistry()
{
    Canvas.willRenderCanvases += PerformUpdate;
}

private void PerformUpdate()
{
    UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
    //清理Queue中值为null或者被销毁的元素
    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))
                    //布局重建,分别传入 Prelayout 、Layout 、PostLayout参数
                    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;

    //执行裁剪(cull)操作
    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))
                {
                    //图形重建,分别传入PreRender、LatePreRender参数
                    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.GraphicUpdateComplete();

    instance.m_GraphicRebuildQueue.Clear();
    m_PerformingGraphicUpdate = false;
    UISystemProfilerApi.EndSample<span class="p">(UISystemProfilerApi.SampleType.Layout);
}
不论是布局重建还是图形重建,都是遍历m_LayoutRebuildQueuem_GraphicRebuildQueue中的元素并调用其Rebuild方法,这些UI元素是怎么被添加进Queue的呢?
对于m_LayoutRebuildQueue,提供了两个公开方法向其添加内容,当元素需要进行布局重建的时候,将调用该函数将自身加入队列,m_GraphicRebuildQueue同样也提供了两个函数。
//向m_LayoutRebuildQueue中添加元素
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

//尝试向m_LayoutRebuildQueue中添加元素
//并返回执行结果(True->成功, False->失败)
public static bool TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

//如果队列中不存在element元素,则添加
private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
    if (m_LayoutRebuildQueue.Contains(element))
        return false;

    return m_LayoutRebuildQueue.AddUnique(element);
}

//向m_GraphicRebuildQueue中添加元素
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

//尝试向m_GraphicRebuildQueue中添加元素
public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    return instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
    if (m_PerformingGraphicUpdate)
    {
        return false;
    }

    return m_GraphicRebuildQueue.AddUnique(element);
}
Graphic为例,我们看一下什么时候会向重建队列中添加元素。
Graphic



Graphic为例(Image和Text间接继承自它),看一下具体发生了什么。
首先是将自身加入重建队列,这里是通过设置“脏数据”实现的,包括布局(Layout)、材质(Material)和顶点(Vertices)三部分,设置布局为脏,将进行布局重建,设置顶点或材质为脏,则进行图形重建。布局重建会将自身加入m_LayoutRebuildQueue中,图形重建则会将自身加入m_GraphicRebuildQueue中,等待被调用。
public virtual void SetAllDirty()
{
    if (m_SkipLayoutUpdate)
    {
        m_SkipLayoutUpdate = false;
    }
    else
    {
        SetLayoutDirty();
    }

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

    SetVerticesDirty();
}

public virtual void SetLayoutDirty()
{
     if (!IsActive())
         return;

     //将元素加入布局重建队列
     LayoutRebuilder.MarkLayutForRebuild(rectTransform);

     Debug.Log("Rebuild:" + rectTransform.name);
     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();
}
加入重建队列之后,CanvasUpdateRegistry就会在PerformUpdate函数中调用它的Rebuild进行重建Graphic实现了接口ICanvasElementRebuild方法,在满足条件的情况下将更新元素的几何网格(UpdateGeometry)和材质(UpdateMaterial)。
public virtual void Rebuild(CanvasUpdate update)
{
    if (canvasRenderer == null || canvasRenderer.cull)
        return;

    switch (update)
    {
        case CanvasUpdate.PreRender:
            if (m_VertsDirty)
            {
                UpdateGeometry();
                m_VertsDirty = false;
            }
            if (m_MaterialDirty)
            {
                UpdateMaterial();
                m_MaterialDirty = false;
            }
            break;
    }
}
UpdateGeometry函数用于确定元素的网格(Mesh)信息,这些信息包括顶点、三角面、UV、颜色等,它们将会被填充到s_VertexHelper中,并最终调用canvasRenderer.SetMesh(workerMesh)设置Mesh信息。
//调用该函数将图形的几何网格更新到CanvasRenderer上。
protected virtual void UpdateGeometry()
{
    //Image、RawImage、Text会在构造函数中将其设置为false
    if (useLegacyMeshGeneration)
    {
        DoLegacyMeshGeneration();
    }
    else
    {
        DoMeshGeneration();
    }
}

private void DoMeshGeneration()
{
    if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
        //UI元素需要生成顶点时的回调函数,用以填充顶点缓冲区的数据
        //其子类重写了这个方法
        OnPopulateMesh(s_VertexHelper);
    else
        s_VertexHelper.Clear();

    //获取当前对象是否有IMeshModifier接口,
    //Text的描边和阴影都是通过它的ModifyMesh方法实现的
    var components = ListPool<Component>.Get();
    GetComponents(typeof(IMeshModifier), components);
<span class="err">
    for (var i = 0; i < components.Count; i++)
        ((IMeshModifier)components).ModifyMesh(s_VertexHelper);

    ListPool<Component>.Release(components);

    s_VertexHelper.FillMesh(workerMesh);
    //设置渲染所需的网格信息
    canvasRenderer.SetMesh(workerMesh);
}
Image

Image间接继承自Graphic,当它的Sprite发生变化时,会调用SetAllDirty函数
public Sprite sprite
{
    get { return m_Sprite; }
    set
    {
        if (m_Sprite != null)
        {
            if (m_Sprite != value)
            {
                m_SkipLayoutUpdate = m_Sprite.rect.size.Equals(value ? value.rect.size : Vector2.zero);
                m_SkipMaterialUpdate = m_Sprite.texture == (value ? value.texture : null);
                m_Sprite = value;

                SetAllDirty();
                TrackSprite();
            }
        }
        else if (value != null)
        {
            m_SkipLayoutUpdate = value.rect.size == Vector2.zero;
            m_SkipMaterialUpdate = value.texture == null;
            m_Sprite = value;

            SetAllDirty();
            TrackSprite();
        }
    }
}
设置Sprite大小的时候也会调用
public override void SetNativeSize()
{
     if (activeSprite != null)
     {
         float w = activeSprite.rect.width / pixelsPerUnit;
         float h = activeSprite.rect.height / pixelsPerUnit;
         rectTransform.anchorMax = rectTransform.anchorMin;
         rectTransform.sizeDelta = new Vector2(w, h);
         SetAllDirty();
     }
}
即对应下图中的SetNativeSize按钮


当然修改Image其他的属性也可能会引发重建,调用的地方太多了,想要进一步了解的同学可以在源码中找到答案。
Text类似,当文本的字体、大小等属性发生变化时,也会引起重建。
总结

以下情形都将进行UI重建,因此在项目中可以针对这些情况进行优化,比如用改变UI的Scale(1->0)来代替改变UI的Enable属性,以ImageScale代替Slider来进行百分比展示等(内容搬运自UGUI UI重建二三事(二) - 知乎 )
<ul><li data-pid="5qoK5baI">Text控件 文本的内容及颜色变化、设置是否支持富文本、更改换行模式、设置字体最大最小值、变更文本使用的对齐锚点、设置是否通过几何对齐、变更字体大小、变更是否支持水平及垂直溢出、修改行间距、变更字体样式(正常、斜体.....)。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-9-22 23:16 , Processed in 0.094252 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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