UGUI核心源码剖析

UGUI核心源码结构


从图4-3可以看出,以文件夹为单位拆分模块,有Culling (裁剪) 、Layout(布局) 、MaterialModifiers (材质球修改器) 、SpecializedCollections(收集)、Utility (实用工具) 、VertexModifiers (页点修改器)。下面对每个模块进行分析。

Culling模块

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

如图4-4所示,文件夹中包含四个文件,其中一个是静态类,一个是接口类。

Clipping类中有两个函数比较重要,常被用在Mask的裁剪上,其源代码如下:

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
/// <summary>
/// Find the Rect to use for clipping.
/// Given the input RectMask2ds find a rectangle that is the overlap of all the inputs.
/// </summary>
/// <param name="rectMaskParents">RectMasks to build the overlap rect from.</param>
/// <param name="validRect">Was there a valid Rect found.</param>
/// <returns>The final compounded overlapping rect</returns>
public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
{
if (rectMaskParents.Count == 0)
{
validRect = false;
return new Rect();
}

Rect current = rectMaskParents[0].canvasRect;
float xMin = current.xMin;
float xMax = current.xMax;
float yMin = current.yMin;
float yMax = current.yMax;
for (var i = 1; i < rectMaskParents.Count; ++i)
{
current = rectMaskParents[i].canvasRect;
if (xMin < current.xMin)
xMin = current.xMin;
if (yMin < current.yMin)
yMin = current.yMin;
if (xMax > current.xMax)
xMax = current.xMax;
if (yMax > current.yMax)
yMax = current.yMax;
}

validRect = xMax > xMin && yMax > yMin;
if (validRect)
return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
else
return new Rect();
}

上述代码中的函数为Clipping类里的函数,第一个函数FindCullAndClipWorldRect()的含义是计算RectMask2D重叠部分的区域。第二个函数Rectlntersect()为第一个函数提供计算服务,其合义是计算两个矩阵的重叠部分。

这两个函数都是静态函数,也可视为工具函数,直接调用即可,不需要实例化。

Layout模块

从图4-5的Layout模块的文件夹结构可以看出,Layout的主要功能都是布局方面的,包括横向布局、纵向布局和方格布局等。总共12个文件,有9个带有Layout字样,它们都是用于处理布局的。

除处理布局内容外,其余3个文件CanvasScaler、AspectRatioFitter、ContentSizeFitter则是用于调整屏幕自适应功能的。

从ContentSizeFitter类、AspectRatioFitter类都带有Fitter字样可以了解到,它们的功能都是处理屏幕自适应。其中ContentSizeFitter类处理的是内容的自适应问题,而AspectRatioFitter类处理的是朝向的自适应问题,包括以长度为基准、以宽度为基准、以父节点为基准、以外层父节点为基准这四种类型的自适应方式。

另外,CanvasScaler类提供的功能非常重要,它操作的是Canvas整个画布针对不同屏幕进行的自适应调整。

由于代码量比较多,这里着重看看CanvasScaler类里的代码,其CanvasScaler类的核心函数源码如下:

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
47
48
49
50
51
52
53
54
55
56
   /// <summary>
/// Handles canvas scaling that scales with the screen size.
/// </summary>
protected virtual void HandleScaleWithScreenSize()
{
Vector2 screenSize = new Vector2(Screen.width, Screen.height);

// Multiple display support only when not the main display. For display 0 the reported
// resolution is always the desktops resolution since its part of the display API,
// so we use the standard none multiple display method. (case 741751)
int displayIndex = m_Canvas.targetDisplay;
if (displayIndex > 0 && displayIndex < Display.displays.Length)
{
Display disp = Display.displays[displayIndex];
screenSize = new Vector2(disp.renderingWidth, disp.renderingHeight);
}

float scaleFactor = 0;
switch (m_ScreenMatchMode)
{
case ScreenMatchMode.MatchWidthOrHeight:
{
// We take the log of the relative width and height before taking the average.
// Then we transform it back in the original space.
// the reason to transform in and out of logarithmic space is to have better behavior.
// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.
// In normal space the average would be (0.5 + 2) / 2 = 1.25
// In logarithmic space the average is (-1 + 1) / 2 = 0
// 在取平均值之前,我们先取相对宽度和高度的对数
// 然后将其转换到原始空间
// 进出对数空间的原因是具有更好的表现
// 如果一个轴的分辨率为两倍,而另一个轴的分辨率为一半
// 则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来计算缩放比例。

MaterialModifiers、SpecializedCollections和Utility

材质球修改器、特殊收集器、实用工具这三部分的逻辑量相对少却相当重要,它们是其他模块所依赖的工具。

MaterialModifiers、SpecializedCollections、 Utility的文件夹结构如图4-6所示。

IMaterialModifier是一个接口类,是为Mask修改材质球所准备的,所用方法需要各自实现。

IndexedSet是一个容器,在很多核心代码上都可使用,它加快了移除元素的速度,并且加快了元素是否包含某个元素的判断操作。

ListPool是List容器对象池,ObiectPool是普通对象池,很多代码上都用到了它们,它们让内存的利用率更高。

VertexHelper特别重要,它用来存储生成网格 (Mesh) 需要的所有数据。在网格生成的过程中,由于顶点的生成频率非常高,因此VertexHelper在存储了网格的所有相关数据的同时,用上面提到的ListPool和ObjectPool作为对象池来生成和回收,使得数据被高效地重复利用,不过它并不负责计算和生成网格,网格的计算和生成由各自的图形组件来完成,它只提供计算后的数据存储服务。

VertexModifiers

VertexModifiers模块的作用是作为顶点修改器。顶点修改器为效果制作提供了更多基础方法和规则。

VertexModifiers模块的文件夹结构如图4-7所示。

VertexModifiers模块主要用于修改图形网格,在UI元素网格生成完毕后可对其进行二次修改。

其中BaseMeshEffect类是抽象基类,提供所有在修改UI元素网格时所需的变量和接口。

IMeshModifier是关键接口,在渲染核心类Graphic中会获取所有拥有这个接的组件,然后依次遍历并调用ModifyMesh接口来触发改变图像网格的效果。

当前在源码中拥有的二次效果包括Outline (包边框)、Shadow (阴影)PositionAsUV1 (位置UV) ,都继承自BaseMeshEffect基类,并实现了关键接ModifyMesh。其中Outline继承自Shadow,它们的共同关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void ApplyShadowZeroAlloc(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
UIVertex vt;

var neededCapacity = verts.Count + end - start;
if (verts.Capacity < neededCapacity)
verts.Capacity = neededCapacity;

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()函数的作用是在原有的网格顶点基础上加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得在原图形外渲染出外描边或者阴影。

核心渲染类

前面剖析的模块在实际业务中是非常有用的工具或算法,它们为核心渲染组件提供了好的基础和方便调用的接口,现在来看看核心渲染类的奥秘所在。

在常用组件Image、Rawlmage、Mask、RectMask2D、Text、InputField中,Image.Rawlmage、Text都继承自MaskableGraphic,而MaskableGraphic又继承自Graphic类,因此Graphic相对比较重要,它是基础类,也存放了核心算法。

除以上这几个类外,CanvasUpdateRegistry是存储和管理所有可绘制元素的管理类,它也是比较重要的类,我们会在下面进行介绍。

首先来看Graphic核心,它有两个部分比较重要,这两个部分揭示了Graphic的运作机制。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
        /// <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.TryRegisterCanvasElementForLayoutRebuild0加入重构队伍,最终重构布局。

SetLayoutDirty()、SetVerticesDirty()、SetMaterialDirty()都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(),被调用时可以认为是通知它去重构网格,但它并没有立即重新构建,而是将需要重构的元件数据加入IndexedSet容器中,等待下次重构。注意,CanvasUpdateRegistry只负责重构网格,并不负责渲染和合并。

我们来看看CanvasUpdateRegistry的RegisterCanvasElementForGraphicRebuild()函数部分,其源码如下:

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
/// <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()将元素放入重构队列中等
待下一次重构。

重构时的逻辑源码如下:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
     private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;

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++)
{
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].GraphicUpdateComplete();

instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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创建自己的网格,然后调用所有需要修改网格的修改者(lMeshModifier) ,也就是效果组件 (描边等效果组件)进行修改,最后放入CanvasRenderer。

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

这里使用VertexHelper是为了节省内存和CPU,它内部采用List容器对象池,将所有使用过的废弃的数据都存储在对象池的容器中,当需要时再拿旧的继续使用,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
   public class VertexHelper : IDisposable
{
private List<Vector3> m_Positions;
private List<Color32> m_Colors;
private List<Vector2> m_Uv0S;
private List<Vector2> m_Uv1S;
private List<Vector2> m_Uv2S;
private List<Vector2> m_Uv3S;
private List<Vector3> m_Normals;
private List<Vector4> m_Tangents;
private List<int> m_Indices;
}

上述代码为VertexHelper的定义部分。

组件中,Image、Rawlmage、Text都override (重写) 了OnPopulateMesh()函数,代码如下:

1
protected override void OnPopulateMesh(VertexHelper toFill)

这些都需要有自己自定义的网格样式来构建不同类型的画面。

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

我试图通过查找反编译的代码来查看相关内容,但也没有找到,我们无法获得这部分的源码但仔细一想,也差不多能想出个大概。合并部分无非就是每次重构时获取Canvas下面所有的CanvasRenderer实例,将它们的网格合并起来,仅此而已。因此关键还是要看如何减少重构次数、提高内存和提高CPU的使用效率。

除Graphic类,Mask部分也是我们关心的问题,继续看Mask部分的核心,源码如下:

1
2
3
4
5
6
7
8
9
10
11
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与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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
     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
// 从切割片中获得合法的Rect
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)
{
if (maskableTarget.canvasRenderer.hasMoved)
maskableTarget.Cull(clipRect, validRect);
}
}

m_LastClipRectCanvasSpace = clipRect;
m_ForceClip = false;
}

从上述源码中可以看到,RectMask2D会先计算并设置裁剪的范围,再对所有子节点调用裁剪操作。其中,由

1
MaskUtilities.GetRectMasksForClip(this, m_Clippers);

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

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

计算需要裁剪的部分,实际上是计算不需要裁剪的部分,其他部分都进行裁剪。最后由

1
2
3
4
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}

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

1
2
3
4
5
6
7
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}

最后的操作是在CanvasRenderer中进行的,前面我们说CanvasRenderer的内容无法得知。但可以很容易想到这里面的操作是什么,即计算两个四边形的相交点,再组合成裁剪后的内容。

至此UGUI的源码剖析已经完毕。其实并没有高深的算法或者技术,所有核心部分都围绕着如何构建网格、谁将重构,以及如何裁剪来进行的。很多性能的关键在于,如何减少重构次数以及提高内存和CPU的使用效率。