基础知识

在UGUI中,每个UI都是Mesh组成的。将scene窗口的Shading Mode改为“WireFrame”,就能查看到UI的网格。

每个UI都开启了模板测试,都在透明物体的渲染队列中渲染。可以把Shading Mode改为“Overdraw”,查看UI的“是否有较多的overdraw”

Drawcall:CPU准备好了所有的顶点、贴图等信息,把这些信息放在GPU可以读取的地方,并向GPU发送一次绘制请求的过程叫做一次Drawcall。

填充率(Fill Rate):显卡每帧或者说每秒能够渲染的像素数。如果overdraw过多,显卡压力就会过大。

批处理(Batching):UGUI做一次Mesh合并的过程。在UGUI中材质和贴图相同并且相邻的UI元素才可以合批。

基础源码简析

Canvas类

UI绘制的底层类,是一个封装在Unity底层的c++类,在Canvas类中有一个很重要的事件willRenderCanvases,这是所有UI在进行过滤、合批后最终要注册的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace UnityEngine
{
[NativeClass("UI::Canvas")]
[NativeClass("Modules/UI/Canvas.h")]
[NativeClass("Modules/UI/UIStructs.h")]
[RequireComponent(typeof(RectTransform))]
public sealed class Canvas : Behaviour
{
public Canvas();
//...
public static event WillRenderCanvases willRenderCanvases;
//...
public delegate void WillRenderCanvases;
}
}

每一个需要显示的UI组件,都会挂载一个Canvas Renderer组件,这些组件就是用来告诉Canvas,哪些UI需要进行绘制,并且向Canvas提供这些UI的Mesh

Graphic类

所有需要显示的组件,比如Image、Text等,都间接继承自Graphic

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
42
43
44
45
46
namespace UnityEngine.UI
{
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
[ExecuteAlways]
public abstract class Graphic
: UIBehaviour,
ICanvasElement
{
//...
[NonSerialized] private bool m_VertsDirty;
[NonSerialized] private bool m_MaterialDirty;
//...
/// <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();
SetRaycastDirty();
}
}
}

ICanvasElement也是一个非常基础的接口,表示继承了Graphic的组件需要在Canvas下显示。如果没有Canvas,就不能进行正常的Mesh重建。在ICanvasElement中,声明了一个非常重要的方法,即ReBubuild方法:

ICanvasElement的接口声明在CanvasUpdateRegistry文件里

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
/// <summary>
/// This is an element that can live on a Canvas.
/// </summary>
public interface ICanvasElement
{
/// <summary>
/// Rebuild the element for the given stage.
/// </summary>
/// <param name="executing">The current CanvasUpdate stage being rebuild.</param>
void Rebuild(CanvasUpdate executing);

/// <summary>
/// Get the transform associated with the ICanvasElement.
/// </summary>
Transform transform { get; }

/// <summary>
/// Callback sent when this ICanvasElement has completed layout.
/// </summary>
void LayoutComplete();

/// <summary>
/// Callback sent when this ICanvasElement has completed Graphic rebuild.
/// </summary>
void GraphicUpdateComplete();

/// <summary>
/// Used if the native representation has been destroyed.
/// </summary>
/// <returns>Return true if the element is considered destroyed.</returns>
bool IsDestroyed();
}

在对UI进行批处理(预先处理)之后,所有带有“脏标记”的UI组件将会进行重建(ReBuild

Graphic类里面进行预处理时,将所有的需要重建的顶点、材质或者Layout设为“脏”,对应的bool设为true。

MaskableGraphic类

MaskableGraphic类是ImageText组件直接继承的类,表示这个组件可以被Mask遮挡。

1
2
3
4
5
6
7
8
9
10
namespace UnityEngine.UI
{
/// <summary>
/// A Graphic that is capable of being masked out.
/// </summary>
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
{
//...
}
}

IMaskable接口表示此图形可以被Mask组件剔除。

LayoutRebuilder类

LayoutRebuilder类是管理各种Layout组件的Rebuild的类。

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
namespace UnityEngine.UI
{
/// <summary>
/// Wrapper class for managing layout rebuilding of CanvasElement.
/// </summary>
public class LayoutRebuilder : ICanvasElement
{
/// <summary>
/// Mark the given RectTransform as needing it's layout to be recalculated during the next layout pass.
/// </summary>
/// <param name="rect">Rect to rebuild.</param>
public static void MarkLayoutForRebuild(RectTransform rect)
{
//...
MarkLayoutRootForRebuild(layoutRoot);
//...
}
private static void MarkLayoutRootForRebuild(RectTransform controller)
{
if (controller == null)
return;

var rebuilder = s_Rebuilders.Get();
rebuilder.Initialize(controller);
if (!CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(rebuilder))
s_Rebuilders.Release(rebuilder);
}
}
}

使用MarkLayoutForRebuild方法,将标记的rect是否需要进行rebuild进行注册CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild(rebuilder)

CanvasUpdateRegistry文件中对应的方法:

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
/// <summary>
/// Try and add the given element to the layout 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 TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
if (m_LayoutRebuildQueue.Contains(element))
return false;

/* TODO: this likely should be here but causes the error to show just resizing the game view (case 739376)
if (m_PerformingLayoutUpdate)
{
Debug.LogError(string.Format("Trying to add {0} for layout rebuild while we are already inside a layout rebuild loop. This is not supported.", element));
return false;
}*/

return m_LayoutRebuildQueue.AddUnique(element);
}

可以看到,在内部所有需要Rebuild的Layout都放在m_LayoutRebuildQueue中。也就是设置成脏标记。

所有可以被Layout影响的组件都继承ILayoutElement接口,Image或Text组件都继承了这个接口

CanvasUpdateRegistry类

这个类负责Canvas更新的注册。不管是Layout的ReBuild还是Graphic的ReBuild都要在这里注册进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace UnityEngine.UI
{
/// <summary>
/// A place where CanvasElements can register themselves for rebuilding.
/// </summary>
public class CanvasUpdateRegistry
{
//...
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
//...
private void PerformUpdate()
{
//先处理Layout的Rebuild队列
//再处理Graphic的Rebuild队列
}
//...
}
}

Mask源码简析

RectMask2D类

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
namespace UnityEngine.UI
{
[AddComponentMenu("UI/Rect Mask 2D", 14)]
[ExecuteAlways]
[DisallowMultipleComponent]
[RequireComponent(typeof(RectTransform))]
/// <summary>
/// A 2D rectangular mask that allows for clipping / masking of areas outside the mask.
/// </summary>
/// <remarks>
/// The RectMask2D behaves in a similar way to a standard Mask component. It differs though in some of the restrictions that it has.
/// A RectMask2D:
/// *Only works in the 2D plane
/// *Requires elements on the mask to be coplanar.
/// *Does not require stencil buffer / extra draw calls
/// *Requires fewer draw calls
/// *Culls elements that are outside the mask area.
/// </remarks>
public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
{
//...
public virtual void PerformClipping()
{
//...
}
}
}

这个类使用IClipper接口来实现核心的遮罩逻辑,核心实现在PerformClipping方法里。

Mask类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace UnityEngine.UI
{
[AddComponentMenu("UI/Mask", 13)]
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
/// <summary>
/// A component for masking children elements.
/// </summary>
/// <remarks>
/// By using this element any children elements that have masking enabled will mask where a sibling Graphic would write 0 to the stencil buffer.
/// </remarks>
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
{
//...
/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
//...
}

}
}

Mask类实现遮罩逻辑并不是通过裁剪,而是使用模板缓存(Stencil Buffer),核心实现在GetModifiedMaterial

拓展:深度测试

在Shader中打开深度测试和深度写入,就能根据物体和摄像机之间的距离来覆盖对应区域的颜色。

比如说在世界空间中,一个黑色物体在白色物体前方,摄像机视野下黑色物体遮挡白色物体,如果我们给白色物体的材质挂载下面的Shader

1
2
3
4
5
6
7
8
9
10
11
Shader "MyShader/AlphaShadera"
{
SubShader
{
ZWrite on//开启深度写入
ZTest Always//深度测试Always模式,此外还有Less模式等,表示不同的覆盖方式
Pass{
Color(1,1,1,1)//表示覆盖成白色
}
}
}

这样在摄像机视野下,白色物体虽然在黑色物体后面,但是被遮挡部分会渲染成白色。

在Unity中透明物体有自己的渲染队列,透明的物体不会写深度,因为所有的透明物体写深度没有意义,必须要全都渲染出来。渲染透明物体时,一般都是从后向前渲染。

所有的UI都是在透明队列中绘制,UI系统中的需要渲染的组件都带有Alpha Blend,所以UI渲染存在overdraw,填充率过大的问题,需要进行优化。