UGUI-Core

16k words

Desc

Core目录下的多个核心模块,Culling(裁剪)、Layout(布局)、MaterialModifiers(材质球修改器)、SpecializedCollections(收集)、Utility(实用工具)、VertexModifiers(顶点修改器)

Culling(裁剪)

对模型裁剪的工具类,大都用在了Mask遮罩上,只有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
40
41
42
43
Clipping.cs

传入 RectMask2D 列表,计算出他们重叠部分的区域
/// <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();
}

Layout(布局)

布局相关

调整屏幕自适应

CanvasScale

操作Canvas整个画布针对不同屏幕进行的自适应调整

代码
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
///<summary>
///Method that handles calculations of canvas scaling.
///</summary>
protected virtual void Handle()
{
if (m_Canvas == null || !m_Canvas.isRootCanvas)
return;

if (m_Canvas.renderMode == RenderMode.WorldSpace)
{
HandleWorldCanvas();
return;
}

switch (m_UiScaleMode)
{
case ScaleMode.ConstantPixelSize: HandleConstantPixelSize(); break;
case ScaleMode.ScaleWithScreenSize: HandleScaleWithScreenSize(); break;
case ScaleMode.ConstantPhysicalSize: HandleConstantPhysicalSize(); break;
}
}

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

MaterialModifiers (材质球修改器)

IMaterialModifier.cs 接口类,为 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
40
41
42
43
44
45
IMaterialModifier.cs
/// <summary>
/// Interface which allows for the modification of the Material used to render a Graphic before they are passed to the CanvasRenderer.
/// </summary>
/// <remarks>
/// When a Graphic sets a material is is passed (in order) to any components on the GameObject that implement IMaterialModifier. This component can modify the material to be used for rendering.
/// </remarks>
public interface IMaterialModifier
{
/// <summary>
/// Perform material modification in this function.
/// </summary>
/// <param name="baseMaterial">The material that is to be modified</param>
/// <returns>The modified material.</returns>
Material GetModifiedMaterial(Material baseMaterial);
}

MaskableGraphic.cs
/// <summary>
/// See IMaterialModifier.GetModifiedMaterial
/// </summary>
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;

if (m_ShouldRecalculateStencil)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
m_ShouldRecalculateStencil = false;
}

// if we have a enabled Mask component then it will
// generate the mask material. This is an optimisation
// it adds some coupling between components though :(
Mask maskComponent = GetComponent<Mask>();
if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
{
var maskMat = StencilMaterial.Add(toUse, (1 << m_StencilValue) - 1, StencilOp.Keep, CompareFunction.Equal, ColorWriteMask.All, (1 << m_StencilValue) - 1, 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMat;
toUse = m_MaskMaterial;
}
return toUse;
}

SpecializedCollections 收集

IndexedSet

//This is a container that gives:
// - Unique items
// - Fast random removal
// - Fast unique inclusion to the end
// - Sequential access
//Downsides:
// - Uses more memory
// - Ordering is not persistent
// - Not Serialization Friendly.

//这是一个容器,提供:
//-独特项目
//-快速随机删除
//-快速、独特的收尾体验
//-顺序访问
//缺点:
//-使用更多内存
//-排序不持久
//-不利于序列化。

//我们使用字典来加速列表查找,这使得保证无重复(set)更便宜
//删除时,我们将最后一个项目移动到删除的项目位置,这样我们只需要更新单个项目的索引缓存。(快速拆卸)
//元素的顺序无法保证。删除将更改项目的顺序。

加快了移除元素的速度,并且加快了元素是否包含某个元素的判断条件

Utility 实用工具

ListPool.cs
// Object pool to avoid allocations.
是 List 容器对象池,ObjectPool 是普通对象池,很多代码上都用到了它们,对象池让内存利用率更高

ObjectPool.cs
普通对象池

它们能让内存的利用率更高

VertexHelper.cs

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

代码
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

private void InitializeListIfRequired()
{
if (!m_ListsInitalized)
{
m_Positions = ListPool<Vector3>.Get();
m_Colors = ListPool<Color32>.Get();
m_Uv0S = ListPool<Vector2>.Get();
m_Uv1S = ListPool<Vector2>.Get();
m_Uv2S = ListPool<Vector2>.Get();
m_Uv3S = ListPool<Vector2>.Get();
m_Normals = ListPool<Vector3>.Get();
m_Tangents = ListPool<Vector4>.Get();
m_Indices = ListPool<int>.Get();
m_ListsInitalized = true;
}
}

/// <summary>
/// Cleanup allocated memory.
/// </summary>
public void Dispose()
{
if (m_ListsInitalized)
{
ListPool<Vector3>.Release(m_Positions);
ListPool<Color32>.Release(m_Colors);
ListPool<Vector2>.Release(m_Uv0S);
ListPool<Vector2>.Release(m_Uv1S);
ListPool<Vector2>.Release(m_Uv2S);
ListPool<Vector2>.Release(m_Uv3S);
ListPool<Vector3>.Release(m_Normals);
ListPool<Vector4>.Release(m_Tangents);
ListPool<int>.Release(m_Indices);

m_Positions = null;
m_Colors = null;
m_Uv0S = null;
m_Uv1S = null;
m_Uv2S = null;
m_Uv3S = null;
m_Normals = null;
m_Tangents = null;
m_Indices = null;

m_ListsInitalized = false;
}
}


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

VertexModifiers

作为顶点修改器,为效果制作提供了更多基础方法和规则
主要用于修改图形网格,尤其是在UI元素网格生成完毕后对其进行二次修改

BaseMeshEffect

抽象基类,提供所有在修改UI元素网格时所需得变量和接口

1
public abstract void ModifyMesh(VertexHelper vh);

IMeshModififer是关键接口,在渲染核心类Graphic中会获取所有拥有这个接口的组件,然后依次遍历并调用ModifyMesh接口来触发改变图像网格的效果
源码中拥有的二次效果包括Outline、Shadow、PositionAsUV1,都继承自BaseMeshEffect基类,并实现了关键接口 ModifyMesh

代码
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
89
90
91
92
93
94
95
96
97
public interface IMeshModifier
{
[Obsolete("use IMeshModifier.ModifyMesh (VertexHelper verts) instead", false)]
void ModifyMesh(Mesh mesh);
void ModifyMesh(VertexHelper verts);
}

[SerializeField]
private Vector2 m_EffectDistance = new Vector2(1f, -1f);
public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive())
return;

var output = ListPool<UIVertex>.Get();
vh.GetUIVertexStream(output);

ApplyShadow(output, effectColor, 0, output.Count, effectDistance.x, effectDistance.y);
vh.Clear();
vh.AddUIVertexTriangleStream(output);
ListPool<UIVertex>.Release(output);
}

/// <summary>
/// Duplicate vertices from start to end and turn them into shadows with the given offset.
/// </summary>
/// <param name="verts">Vert list to copy</param>
/// <param name="color">Shadow color</param>
/// <param name="start">The start index in the verts list</param>
/// <param name="end">The end index in the vers list</param>
/// <param name="x">The shadows x offset</param>
/// <param name="y">The shadows y offset</param>
protected void ApplyShadow(List<UIVertex> verts, Color32 color, int start, int end, float x, float y)
{
ApplyShadowZeroAlloc(verts, color, start, end, x, y);
}


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

outline相对于shadow的区别,对四个方向进行处理

public override void ModifyMesh(VertexHelper vh)
{
if (!IsActive())
return;

var verts = ListPool<UIVertex>.Get();
vh.GetUIVertexStream(verts);

var neededCpacity = verts.Count * 5;
if (verts.Capacity < neededCpacity)
verts.Capacity = neededCpacity;

var start = 0;
var end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, effectDistance.y);

start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, -effectDistance.y);

start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, effectDistance.y);

start = end;
end = verts.Count;
ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, -effectDistance.y);

vh.Clear();
vh.AddUIVertexTriangleStream(verts);
ListPool<UIVertex>.Release(verts);
}

使用Shadow时,似乎会破坏Unity顶点数的优化,顶点数会变化

Image text
Image text
Image text
Image text

核心渲染类

Graphic.cs
布局重建和图形重建前面以及写了
SetLayoutDirty()、SetVerticesDirty()、SetMaterialDirty()都调用了CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(),被调用时可以认为是通知它去重建网格,但它并没有立即重新构建,而是将需要重建的元件数据加入IndexedSet容器中,等待下次重构

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

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

重构部分:
PerformUpdate:将要重新布局的元素取出来,一个个调用Rebuild函数重构,再对布局后的元素进行裁剪,裁剪后将布局中每个需要重构的元素取出来并调用Rebuild函数进行重构,最后做一些清理的事务
执行网格构建函数
DoMeshGeneration():
OnPopulateMesh():创建自己的网格,然后调用所有需要修改网格的修改者(IMeshModifier),也就是效果组件(描边等效果组件)进行修改,最后放入CanvasRender
VertexHelper是为了节省内存和CPU,内部采用List容器对象池,将所有使用过的废弃的数据都存储在对象池的容器中,当需要时再拿旧的继续使用

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

CanvasRenderer和Canvas是合并网格的关键,但是没有开源

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

代码
1
2
3
4
5
6
7
8
9
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);

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
82
83
84
// 计算并设置裁剪的范围,再对所有子节点调用裁剪操作
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)
{
// 获取所有有关联的RectMask2D Mask范围
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)
{
// 对所有需要裁剪的UI元素进行裁剪操作
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;
}