Mask基本知识

Mask的原理是模板缓存。

Mask必须依赖一个Image组件。

一个单独的Mask(没有任何用来遮罩的子物体),也会产生2个drawcall。

用作Mask的Image,是无法与相邻的同材质的Image进行合批的,Mask会打断合批(因为Mask会使用特殊的材质)。同样地,Mask的子Image只能在Mask内部进行合批,和外部的Image无法合批。两个Mask使用的图片贴图ID相同的话,也能够合批。

Mask源码

Mask自己本身产生两次drawcall的原因:

  1. Mask在最开始设置模板缓存时,会产生一次drawcalll。
  2. 在Mask遍历完所有子物体执行完逻辑后,会还原一下模板缓存,这会产生第二次drawcall。

在Mask源码的GetModifiedMaterial方法内,写明了模板缓存的逻辑:

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
/// Stencil calculation time!
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
if (!MaskEnabled())
return baseMaterial;

var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
if (stencilDepth >= 8)
{
Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
return baseMaterial;
}

int desiredStencilBit = 1 << stencilDepth;

// if we are at the first level...
// we want to destroy what is there
if (desiredStencilBit == 1)
{
var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);//核心方法,为了开始模板缓存,需要给Mask一个特殊的材质,这里就增加了一次drawcall,这也是Mask打断合批的原因
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;
}

//otherwise we need to be a bit smarter and set some read / write masks
var maskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit | (desiredStencilBit - 1), StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial2;

graphic.canvasRenderer.hasPopInstruction = true;
var unmaskMaterial2 = StencilMaterial.Add(baseMaterial, desiredStencilBit - 1, StencilOp.Replace, CompareFunction.Equal, 0, desiredStencilBit - 1, desiredStencilBit | (desiredStencilBit - 1));
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial2;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);

return m_MaskMaterial;
}

模板缓存的简单解释

模板缓存是在渲染流水线最后逐片元操作部分发挥作用的,是透明渲染队列中确认一个片元是否需要被渲染的首要操作,它的作用就是将片元的显示范围限制在一定的区域内。

模板测试

图中数字1表示能显示的区域,数字0表示被舍弃的片元。整个数字表称为模板缓存。这套缓存是可以开发人员自己配置。

Mask第一次drawcall就是产生这些模板缓存的值。

将Mask下的UI元素按照模板值进行渲染后,模板缓存的值还要进行还原来准备下一次绘制,这就产生了第二次drawcall

Mask注意要点

Mask下的UI元素完全在Mask之外,也就是说Mask下有些UI元素无法在屏幕中绘制出来了,也依然会占用drawcall(当然把这些无法绘制的UI元素关闭的话就不占用了)。

Mask内部的子UI元素之间是可以合批的。

同级的Mask之间只要使用相同的遮罩贴图,也是能够合批的。而只要两个Mask能够合批,这两个Mask的子物体就相当于在同一个Mask下,是能够相互合批的。

需要注意的是,Mask下的UI组件即使有些部分不显示了,但是不显示的部分也是参与深度测试的,如果这些看不见的部分和其他的UI发生了重叠,也是会影响合批的。

RectMask2D

RectMask2D比Mask更节省性能,这是因为RectMask2D本身并不占用Drawcall。但这也是相对的,RectMask组件自身并不能合批,所以不适合大量使用。

原理

下面是RectMask2D源码中的主逻辑PerformClipping方法

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);//通过这里获取Rect
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)
{
clipTarget.SetClipRect(clipRect, validRect);//被剪切的目标自己执行SetClipRect方法
}

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)
{
//Case 1170399 - hasMoved is not a valid check when animating on pivot of the object
maskableTarget.Cull(clipRect, validRect);
}
}

m_LastClipRectCanvasSpace = clipRect;
m_ForceClip = false;

UpdateClipSoftness();
}

具体的剪切执行逻辑在CanvasRenderer里面,这涉及内部C++执行逻辑,这是为了更好性能。

简而言之,RectMask2D的原理就是给自己的子UI元素传一个区域,让子UI元素根据这个区域自己进行绘制。而且子UI元素被排除掉的部分连顶点也不绘制了。如果RectMask2D的子UI元素全部都在Mask区域外部,那么整个UI都不会占用drawcall了。

注意要点

RectMask和Mask一样,会打断合批。但是同一个ReckMask2D内部的子UI元素可以进行合批。

同级的RectMask2D之间无法合批,同理同级的RectMask2D之间的子UI元素也无法合批。但是同级的RectMask2D组件下都带一个Iamge组件时只要使用相同的贴图可以合批

在实际使用Mask过程中,需要根据需要选择使用Mask还是RectMask2D