学习博客:

https://blog.csdn.net/qq_28820675/article/details/105746002

https://blog.csdn.net/gaojinjingg/article/details/103565840

更好的入门文章:

https://zhuanlan.zhihu.com/p/448293298

img

由Canvas控制,通过 ICanvasElement 接口,使用脏标记方法来统一更新CanvasElement

扫盲

摘自大佬博客

  • Canvas, 是Unity渲染系统给层状几何体( layered geometry )提供的可以被画入、被放在上面或者放在世界空间的底层Unity组件。Canvas负责将它包含的几何体组合成batch,生成合适的渲染命令发送给Unity图形系统。这个过程在底层的C++代码中完成,这个过程被称为一次rebatch或者一次batch build。当一个Canvas被标记为包含需要rebatch的几何体时,这个Canvas被认为是dirty的。
  • layered geometry , 由Canvas Renderer组件提供给Canvas。[ Canvas 负责进行渲染, Canvas Renderer负责采集/接收. ]
  • **动静隔离 **, 一个子Canvas仅仅是一个嵌套在父Canvas中的组件,子Canvas将它的子物体和它的父Canvas隔离,一个子Canvas下dirty的子物体不会触发父Canvas的rebuild,反之亦然。(这些在某些特殊情况下是不确定的,比如说改变父Canvas的大小导致子Canvas的大小改变。)
  • Graphic , 是UGUI的C#库提供的一个基类。它是UGUI所有类的基类,给所有的UGUI类提供可以画在Canvas系统上的几何图形。大多数Unity内置的继承Graphic的类都是通过继承一个叫MaskableGraphic的子类来实现,这使得他们可以通过IMaskable接口来被隐藏。Drawable类的子类主要是image和text,已经提供了同名的组件。
  • Layout , 组件控制着RectTransform的大小和位置,经常被用于要生成具有相似的大小和位置关系内容的复杂布局。它只依靠RectTransform,只影响与其相关的RectTransform的属性。这些layout组件不依赖于Graphic类,可以独立于UGUI的Graphic组件之外使用。
  • CanvasUpdateRegistry , Graphic和Layout组件都依赖于CanvasUpdateRegistry类,它不会在Unity编辑器的界面中显示。这个类追踪那些Graphic和Layout组件必须被更新的时候,还有与其对应的Canvas触发了willRenderCanvases事件的时候。更新Graphic类和Layout类叫做rebuild。
  • Rebuild , 过程是指Layout和UGUI的C#的Graphic组件的网格被重新计算,这是在CanvasUpdateRegistry类中执行的。这是一个C#类,它的源码可以在Unity的Bitbucket上找到。
    CanvasUpdateRegistry , 类中,PerformUpdate方法,当一个Canvas组件触发它的WillRenderCanvases事件时,这个方法就会被执行。这个事件每帧调用一次。
    PerformUpdate , 函数运行的三个步骤:
    1- 通过ICanvasElement.Rebuild函数,请求rebuild被Dirty的Layout组件。
    2- 所有被注册的裁剪组件(例如Mask),对需要被裁剪的组件进行剔除。这在ClippingRegistry.Cull中执行。
    3- dirty的Graphic组件被要求rebuild其图形元素。
  • Layout Rebuild , 要重新计算一个或者多个Layout组件所包含的UI组件的适当位置(以及可能的大小),有必要对Layout应用层次的排序。在GameObject的hierarchy中靠近root的Layout可能会影响改变嵌套在它里面的其他Layout的位置和大小,所以必须首先计算。
  • Graphic Rebuild , 当Graphic组件被rebuild的时候,UGUI将控制传递给ICanvasElement接口的Rebuild方法。Graphic执行了这一步,并在rebuild过程中的PreRender阶段运行了两个不同的rebuild步骤:1.如果顶点数据已经被标为Dirty(例如组件的RectTransform已经改变大小),则重建网格。2.如果材质数据已经被标为Dirty(例如组件的material或者texture已经被改变),则关联的Canvas Renderer的材质将被更新。Graphic的Rebuild不会按照Graphic组件的特殊顺序进行,也不会进行任何的排序操作。

入门理解

这里对大佬的文章进行摘要。

Unity是怎么绘制UI元素的?

Unity中渲染的物体都是由网格(Mesh)构成的,而网格的绘制单元是图元(点、线、三角面)。在unity中添加一个ImageText,并且将Shadings Mode设置为Wireframe模式,可以看到一个Image由四个顶点和两个三角面构成,Text也是由许多顶点和三角面构成。

绘制信息都存储在Vertexhelper类中,除了顶点外,还包括法线、UV、颜色、切线以及一些函数。

数据存储好了,那怎么绘制呢?

这是依靠CanvasRenderer来完成的,它听起来可能比较陌生,但实际上当我们在项目中创建的一些UI元素,比如ButtonImageText时,都包含组件CanvasRenderer,这个类提供了许多关键绘制信息,比如被渲染物体的颜色、材质和Mesh等,主要作用就是渲染包含在Canvas中的UI对象,但是在Inspector界面中并不会展示任何属性。

总结一下就是,Unity会把要绘制的UI信息保存在Vertexhelper中,并且调用CanvasRenderer里面的方法进行绘制。

重建 Rebuild

UI重建分为两类,一类是布局重建(Layout Rebuild),另一类是图形重建(Graphic Rebuild)。

一个UI若要重建,必须继承自ICanvasElement接口,因为执行重建操作的时候会调用接口中的Rebuild函数。

Canvas

是Unity渲染系统给层状几何体( layered geometry )提供的可以被画入、被放在上面或者放在世界空间的底层Unity组件。

Canvas在渲染前会调用willRenderCanvases事件,也就是Registry的PerformUpdate方法,用委托的形式传进去的。

CanvasUpdateRegistry

画面刷新的注册工具类,在它的构造函数中会给Canvas注册回调:Canvas.willRenderCanvases += PerformUpdate;

内部维护2个队列(都是 ICanvasElement类型 的):

  • LayoutRebuildQueue:布局重建队列
  • GraphicRebuildQueue:图像重建队列

这2个队列提供了公开方法向其添加内容。

1
2
3
4
5
6
7
8
9
10
11
//向m_LayoutRebuildQueue中添加元素
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

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

添加内容时机

那么什么时候对2个重建队列添加内容,也就是怎么确定哪些要重建呢?是通过脏数据实现的。

布局(Layout)、材质(Material)、顶点(Vertices)三部分,设置布局为脏,将进行布局重建;设置顶点或材质为脏,则进行图形重建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public virtual void SetAllDirty()
{
// 略了,就是根据一些flag依次调用下面3个方法
}

public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
//将元素加入布局重建队列
LayoutRebuilder.MarkLayoutForRebuild(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对Rebuild进行了实现:如果顶点或材质被标记为“脏”的话,会更新元素的几何网格(UpdateGeometry)和材质(UpdateMaterial)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);

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

渲染前流程

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

Rebuild时机:脏标记

这里用脏标记,就是将重建的行为延迟到用户需要这个物体的时候才执行,一种优化重新渲染的手段。

在Graphic 中存在三种脏标记分别代表三种等待重建

  • 尺寸改变时(RectTransformDimensions):LayoutRebuild 布局重建

  • 尺寸、颜色改变时:Vertices to GraphicRebuild 图像顶点重建

  • 材质改变时:Material to GraphicRebuild 图像材质重建

层级改变、应用动画属性(DidApplyAnimationProperties) :All to Rebuild 重建所有

案例1:Image

举例Image的情况,Image间接继承自Graphic,当它的Sprite发生变化时,会调用SetAllDirty函数;设置Sprite大小的时候也会调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
}
}
}

案例more

  • Text控件 文本的内容及颜色变化、设置是否支持富文本、更改换行模式、设置字体最大最小值、变更文本使用的对齐锚点、设置是否通过几何对齐、变更字体大小、变更是否支持水平及垂直溢出、修改行间距、变更字体样式(正常、斜体…..)。
  • Image控件 颜色变化、变更显示类型(SimpleSlicedTiledFilled)、变更是否应保留Sprite宽高比(Image.preserveAspect属性的变更),FillCenter属性变更(是否渲染平铺或切片图像的中心)、变更填充方式(HorizontalVerticalRadial360….)、变更图像填充率(fillAmount)、变更图像顺逆时针填充类型(Image.fillClockwise)、变更填充过程的原点(Image.FillOrigin)。
  • RawImage控件 设置Texture、变更纹理使用的UVRcet
  • Shadow效果 改变效果的距离(effectDistance)及颜色(effectColor)、变更是否使用Graphic中的Alpha透明度(useGraphicAlpha)。
  • Mask控件 设置是否展示与Mask渲染区域相关的图形(showMaskGraphic),enable发生变化
  • 所有继承MaskableGraphic的控件(ImageRawImageRectMask2DText) 设置此图形是否允许被遮盖、enable发生变化、父节点发生变化(TransFromParentChanged)、在Hierachy面板上发生改变(HierachyChanged)。
  • 所有继承自BaseMeshEffect的效果类(目前只看到ShadowPositionAsUV1)的enable变化及应用动画属性的操作。
  • 所有继承自Graphic的UI控件材质(material)发生变化。

一整轮Rebuild:PerformUpdate方法

1.在布局重建队列、图像重建队列中,剔除已销毁对象

2.更新布局。根据父节点数量排序,先深后浅。更新类型依次为 Prelayout 、Layout 、PostLayout。详细见ugui_4。

3.执行LayoutComplete回调,也就是通知LayoutRebuild队列的所有元素,通知布局已完成

4.布局完成,可以对UI(IClipper)进行裁剪了,显示不到的就不渲染

5.更新图像。依次 PreRender、LatePreRender、MaxUpdateValue:1.如果顶点数据已经被标为Dirty(例如组件的RectTransform已经改变大小),则重建网格。2.如果材质数据已经被标为Dirty(例如组件的material或者texture已经被改变),则关联的Canvas Renderer的材质将被更新。

6.执行GraphicUpdateComplete回调,通知图像更新完成

顺序枚举

CanvasUpdate,一个枚举类,很核心,代表着Canvas对Layout、Render的处理顺序:

  1. Prelayout:Called before layout.
  2. Layout
  3. PostLayout:Called after layout.
  4. PreRender:Called before rendering.
  5. LatePreRender:Called late, before render.
  6. MaxUpdateValue:Max enum value. Always last.