UGUI组件

13k words

Desc:

UGUI是在3d网格下建立的UI系统,每个可显示的元素都是通过3D模型网格的形式构建起来的。当UI系统被实例化时,UGUI系统首先要做的就是构建网格

运行原理:

如果每个元素都生成一个模型且绑定一个材质球存入一张图片,那么界面上成千上万个元素就会拥有成千上万个材质球,以及成千上万张图。UGUI对此的优化是,将一部分相同类型的图片集合起来合成一张图,然后将拥有相同图片、相同着色器的材质球指向同一个材质球,并且将分散开的模型、网格合并起来,这样就生成几个大网格和几个不同图集的材质球,以及少许整张的贴图,节省了很多材质球、图片、网格的渲染,提升UI系统的效率

注意:

并不是把所有网格和材质球都合并成一个,因为这样模型前后层级就会有问题,只是把相同层级的元素,以及相同层级上拥有相同材质球参数的网格进行合并处理。

优化的方式就是合并更多的元素,减少重构网格的次数 (合并网格中的模型如果发生移动或者销毁会导致网格重新构建)

RawImage组件能展示单张图片,但无法参与合并
(不适用图集而使用RawImage展示单张图片时,通常是因为图片尺寸过大而导致合并图集效率低,或者相同类型的图片数量太多)

Canvas:

Canvas能够将子物体UI元素进行合并,合并的规则是,在一个Canvas里,将相同层级、相同材质球的元素进行合并,从而减少DrawCall.但相同层级并不是指gameObject上的节点层级,而是覆盖层级。Canvas里如果两个元素重叠,则可以认为它们是上下层关系,将所有重叠的层级数排列顺序计算完毕后,将从第0层开始的同一层级的元素合并,再将第1、2、3…层的元素同样进行合并,一次类推其他层(没懂)

Image text
Image text

合并跟节点层级的顺序没有关系,中间插入一个其他图集的不会导致dc变多

Egret中如果相同节点层级中间插入一个其他图集的图片,会导致dc增加
Image text
Image text

Canvas Scaler:

屏幕适配组件,用来指定画布中元素的比例大小。
简单指定比例大小的 Constant Pixel Size
以屏幕为基准的自动适配比例大小的规则 Scale With Screen Size (在手游中,设备的分辨率变化比较大,通常使用以屏幕为基准的自动适配比例大小的Scale With Screen Size)
以物理大小为基准的适配规则 Constant Physical Size

Graphic Raycaster:

输入系统的图形碰撞测试组件,并不会检测Canvas以外的内容,检测的都是Canvas下的元素。当图元素上存在有效碰撞体时,Graphic Raycaster组件会统一使用射线碰撞检测来测试碰撞的元素。也可以设置完全忽略输入的方式来彻底取消点击响应,还可以指定阻止对某些layers进行响应
StandaloneInputModule 模块中,Process 函数中,当发生点击事件时,调用 ProcessMouseEvent 函数时,通过 GetMousePointerEventData 函数进行射线检测 eventSystem.RaycastAll ,RaycasterManager.GetRaycasters 中存放的是所有 Canvas 组件上的 GraphicRaycaster 组件,跟据鼠标位置转换到屏幕坐标空间位置后,判断鼠标是否在画布下元素位置内且元素的 raycastTarget 不为true等条件判断,得到射线检测后的结果
Image text

EventTrigger:

输入事件触发器,与此脚本绑定的UI物体都可以接受输入事件。比如(鼠标、手指)按下、弹起、点击、开始拖拽、拖拽中、结束拖拽、鼠标滚动事件等。起到点击响应作用,配合前面的 Graphic Raycaster 进行响应

Graphic:

大部分UI组件的基类,当修改部分UI数据时,设置脏数据可以进行UI重建
Image text
Image text
Image text

Image:

继承于 MaskableGraphic,ETC涉及到打包安卓时做的Alpha通道分离
Image text
Image 默认的材质中 Shader 选用的是 UI/Default,透明度混合和透明度剔除
Image text
Image text
URP中,2d贴图能够接受光照,具体实现,似乎是将2d光源渲染到rt图中,根据图片像素位置得到屏幕空间坐标后,对rt图进行采样得到相应的光照颜色
Image text

代码
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
#if !defined(COMBINED_SHAPE_LIGHT_PASS)
#define COMBINED_SHAPE_LIGHT_PASS

half _HDREmulationScale;
half _UseSceneLighting;
half4 _RendererColor;

half4 CombinedShapeLightShared(half4 color, half4 mask, half2 lightingUV)
{
if (color.a == 0.0)
discard;

color = color * _RendererColor; // This is needed for sprite shape

#if USE_SHAPE_LIGHT_TYPE_0
half4 shapeLight0 = SAMPLE_TEXTURE2D(_ShapeLightTexture0, sampler_ShapeLightTexture0, lightingUV);

if (any(_ShapeLightMaskFilter0))
{
half4 processedMask = (1 - _ShapeLightInvertedFilter0) * mask + _ShapeLightInvertedFilter0 * (1 - mask);
shapeLight0 *= dot(processedMask, _ShapeLightMaskFilter0);
}

half4 shapeLight0Modulate = shapeLight0 * _ShapeLightBlendFactors0.x;
half4 shapeLight0Additive = shapeLight0 * _ShapeLightBlendFactors0.y;
#else
half4 shapeLight0Modulate = 0;
half4 shapeLight0Additive = 0;
#endif

#if USE_SHAPE_LIGHT_TYPE_1
half4 shapeLight1 = SAMPLE_TEXTURE2D(_ShapeLightTexture1, sampler_ShapeLightTexture1, lightingUV);

if (any(_ShapeLightMaskFilter1))
{
half4 processedMask = (1 - _ShapeLightInvertedFilter1) * mask + _ShapeLightInvertedFilter1 * (1 - mask);
shapeLight1 *= dot(processedMask, _ShapeLightMaskFilter1);
}

half4 shapeLight1Modulate = shapeLight1 * _ShapeLightBlendFactors1.x;
half4 shapeLight1Additive = shapeLight1 * _ShapeLightBlendFactors1.y;
#else
half4 shapeLight1Modulate = 0;
half4 shapeLight1Additive = 0;
#endif

#if USE_SHAPE_LIGHT_TYPE_2
half4 shapeLight2 = SAMPLE_TEXTURE2D(_ShapeLightTexture2, sampler_ShapeLightTexture2, lightingUV);

if (any(_ShapeLightMaskFilter2))
{
half4 processedMask = (1 - _ShapeLightInvertedFilter2) * mask + _ShapeLightInvertedFilter2 * (1 - mask);
shapeLight2 *= dot(processedMask, _ShapeLightMaskFilter2);
}

half4 shapeLight2Modulate = shapeLight2 * _ShapeLightBlendFactors2.x;
half4 shapeLight2Additive = shapeLight2 * _ShapeLightBlendFactors2.y;
#else
half4 shapeLight2Modulate = 0;
half4 shapeLight2Additive = 0;
#endif

#if USE_SHAPE_LIGHT_TYPE_3
half4 shapeLight3 = SAMPLE_TEXTURE2D(_ShapeLightTexture3, sampler_ShapeLightTexture3, lightingUV);

if (any(_ShapeLightMaskFilter3))
{
half4 processedMask = (1 - _ShapeLightInvertedFilter3) * mask + _ShapeLightInvertedFilter3 * (1 - mask);
shapeLight3 *= dot(processedMask, _ShapeLightMaskFilter3);
}

half4 shapeLight3Modulate = shapeLight3 * _ShapeLightBlendFactors3.x;
half4 shapeLight3Additive = shapeLight3 * _ShapeLightBlendFactors3.y;
#else
half4 shapeLight3Modulate = 0;
half4 shapeLight3Additive = 0;
#endif

half4 finalOutput;
#if !USE_SHAPE_LIGHT_TYPE_0 && !USE_SHAPE_LIGHT_TYPE_1 && !USE_SHAPE_LIGHT_TYPE_2 && ! USE_SHAPE_LIGHT_TYPE_3
finalOutput = color;
#else
half4 finalModulate = shapeLight0Modulate + shapeLight1Modulate + shapeLight2Modulate + shapeLight3Modulate;
half4 finalAdditve = shapeLight0Additive + shapeLight1Additive + shapeLight2Additive + shapeLight3Additive;
finalOutput = _HDREmulationScale * (color * finalModulate + finalAdditve);
#endif

finalOutput.a = color.a;

finalOutput = finalOutput *_UseSceneLighting + (1 - _UseSceneLighting)*color;
return max(0, finalOutput);
}
#endif

RawImage:

似乎只有在设置 texture 和 uvRect 时,会触发UI重建

代码
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
public Texture texture
{
get
{
return m_Texture;
}
set
{
if (m_Texture == value)
return;

m_Texture = value;
SetVerticesDirty();
SetMaterialDirty();
}
}

/// <summary>
/// UV rectangle used by the texture.
/// </summary>
public Rect uvRect
{
get
{
return m_UVRect;
}
set
{
if (m_UVRect == value)
return;
m_UVRect = value;
SetVerticesDirty();
}
}

Mask

射线检测时,在Raycast函数中,会获取Graphic身上的ICanvasRaycasterFilter组件,Mask继承于ICanvasRaycastFilter类,并实现了 IsRaycastLocationValid 方法,用于判断是否点击到遮罩

1
2
3
4
5
6
7
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return true;

return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
}

设置showMaskGraphic后的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
public bool showMaskGraphic
{
get { return m_ShowMaskGraphic; }
set
{
if (m_ShowMaskGraphic == value)
return;

m_ShowMaskGraphic = value;
if (graphic != null)
graphic.SetMaterialDirty();
}
}

Mask在剔除中的处理,使用模块测试的方式实现
Image text

代码
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
Mask.cs
MaskUtilities.NotifyStencilStateChanged(this);

---------------------------------------------------------

MaskUtilities.cs
/// <summary>
/// Notify all IMaskable under the given component that they need to recalculate masking.
/// </summary>
/// <param name="mask">The object thats changed for whose children should be notified.</param>
public static void NotifyStencilStateChanged(Component mask)
{
var components = ListPool<Component>.Get();
mask.GetComponentsInChildren(components);
for (var i = 0; i < components.Count; i++)
{
if (components[i] == null || components[i].gameObject == mask.gameObject)
continue;

var toNotify = components[i] as IMaskable;
if (toNotify != null)
toNotify.RecalculateMasking();
}
ListPool<Component>.Release(components);
}

---------------------------------------------------------

MaskableGraphic.cs
/// <summary>
/// See IMaskable.RecalculateMasking
/// </summary>
public virtual void RecalculateMasking()
{
// Remove the material reference as either the graphic of the mask has been enable/ disabled.
// This will cause the material to be repopulated from the original if need be. (case 994413)
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = null;
m_ShouldRecalculateStencil = true;
SetMaterialDirty();
}

可以把UI模型中模块测试部分注释来检测

RectMask2D:

射线检测时,在Raycast函数中,会获取Graphic身上的ICanvasRaycasterFilter组件,RectMask2D继承于ICanvasRaycastFilter类,并实现了 IsRaycastLocationValid 方法,用于判断是否点击到遮罩

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
RectMask2D.cs
MaskUtilities.Notify2DMaskStateChanged(this);

---------------------------------------------------------

MaskUtilities.cs
/// <summary>
/// Notify all IClippables under the given component that they need to recalculate clipping.
/// </summary>
/// <param name="mask">The object thats changed for whose children should be notified.</param>
public static void Notify2DMaskStateChanged(Component mask)
{
var components = ListPool<Component>.Get();
mask.GetComponentsInChildren(components);
for (var i = 0; i < components.Count; i++)
{
if (components[i] == null || components[i].gameObject == mask.gameObject)
continue;

var toNotify = components[i] as IClippable;
if (toNotify != null)
toNotify.RecalculateClipping();
}
ListPool<Component>.Release(components);
}

---------------------------------------------------------

MaskableGraphic.cs
/// <summary>
/// See IClippable.RecalculateClipping
/// </summary>
public virtual void RecalculateClipping()
{
UpdateClipParent();
}

private void UpdateClipParent()
{
var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null;

// if the new parent is different OR is now inactive
if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
{
m_ParentMask.RemoveClippable(this);
UpdateCull(false);
}

// don't re-add it if the newparent is inactive
if (newParent != null && newParent.IsActive())
newParent.AddClippable(this);

m_ParentMask = newParent;
}

---------------------------------------------------------

MaskUtilities.cs
/// <summary>
/// Find the correct RectMask2D for a given IClippable.
/// </summary>
/// <param name="clippable">Clippable to search from.</param>
/// <returns>The Correct RectMask2D</returns>
public static RectMask2D GetRectMaskForClippable(IClippable clippable)
{
List<RectMask2D> rectMaskComponents = ListPool<RectMask2D>.Get();
List<Canvas> canvasComponents = ListPool<Canvas>.Get();
RectMask2D componentToReturn = null;

clippable.gameObject.GetComponentsInParent(false, rectMaskComponents);

if (rectMaskComponents.Count > 0)
{
for (int rmi = 0; rmi < rectMaskComponents.Count; rmi++)
{
componentToReturn = rectMaskComponents[rmi];
if (componentToReturn.gameObject == clippable.gameObject)
{
componentToReturn = null;
continue;
}
if (!componentToReturn.isActiveAndEnabled)
{
componentToReturn = null;
continue;
}
clippable.gameObject.GetComponentsInParent(false, canvasComponents);
for (int i = canvasComponents.Count - 1; i >= 0; i--)
{
if (!IsDescendantOrSelf(canvasComponents[i].transform, componentToReturn.transform) && canvasComponents[i].overrideSorting)
{
componentToReturn = null;
break;
}
}
break;
}
}

ListPool<RectMask2D>.Release(rectMaskComponents);
ListPool<Canvas>.Release(canvasComponents);

return componentToReturn;
}

---------------------------------------------------------

RectMask2D.cs
/// <summary>
/// Add a IClippable to be tracked by the mask.
/// </summary>
/// <param name="clippable">Add the clippable object for this mask</param>
public void AddClippable(IClippable clippable)
{
if (clippable == null)
return;
m_ShouldRecalculateClipRects = true;
MaskableGraphic maskable = clippable as MaskableGraphic;

if (maskable == null)
m_ClipTargets.Add(clippable);
else
m_MaskableTargets.Add(maskable);

m_ForceClip = true;
}


foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}

---------------------------------------------------------

MaskableGraphics.cs
/// <summary>
/// See IClippable.SetClipRect
/// </summary>
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);//通过设置着色器去实现遮罩
else
canvasRenderer.DisableRectClipping();
}

Text:

Unity中Text会作为Image去渲染,其中贴图是从字体材质的mainTexture,字体通过采样定点色和采样贴图结果的alpha去显示具体形状

代码
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
    /// <summary>
/// Text's texture comes from the font.
/// </summary>
public override Texture mainTexture
{
get
{
if (font != null && font.material != null && font.material.mainTexture != null)
return font.material.mainTexture;

if (m_Material != null)
return m_Material.mainTexture;

return base.mainTexture;
}
}

public Font font
{
get
{
return m_FontData.font;
}
set
{
if (m_FontData.font == value)
return;

FontUpdateTracker.UntrackText(this);

m_FontData.font = value;

FontUpdateTracker.TrackText(this);

#if UNITY_EDITOR
// needed to track font changes from the inspector
m_LastTrackedFont = value;
#endif

SetAllDirty();
}
}


Image text

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
o.color=v.color;
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
return i.color;
return col;
}

相对于NGUI的Laebl,UGUI的Text不能进行原生的颜色渐变,如果要实现,可以考虑修改Text的定点色,因为每个字符在字体材质中的贴图中uv不一定是一致的,所以根据uv去修改颜色会产生不可控的颜色
u:
Image text
v:
Image text
Image text
Image text

要看字体材质中的贴图,可以使用如下脚本

代码
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
GetTextMainTexture.cs

public Text targetText;
public RenderTexture outputRt;
public Material computeMaterial;
// Start is called before the first frame update
void Start()
{
if(targetText && outputRt){
Graphics.Blit(targetText.mainTexture,outputRt,computeMaterial);
}
}

---------------------------------------------------------

CopyTexture.shader
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}

通过输出的结果可以知道,字体材质中的mainTexture只是修改了alpha通道,对于rgb通道并没有处理
Image text
Image text