Unity3D UGUI 源码学习 Mask

UI结构中的Mask组件用于对其子节点产生遮罩。通过修改模板值来实现其子结点中各MaskableGraphic组件的遮罩控制。阅读本文之前强烈建议先阅读Unity3D UGUI 源码学习 MaskableGraphic

1
2
3
4
5
[AddComponentMenu("UI/Mask", 13)]
[ExecuteInEditMode]
[RequireComponent(typeof(RectTransform))]
[DisallowMultipleComponent]
public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier

生命周期

Mask覆写了UIBehaviour中的一些方法:在OnEnableOnDisable中会对其自身的Graghic组件进行对应操作,重置材质脏标记、设置CanvasRendererhasPopInstruction属性等。最后调用MaskUtilities.NotifyStencilStateChanged方法,并传入自己为参数。

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
protected override void OnEnable()
{
base.OnEnable();
if (graphic != null)
{
graphic.canvasRenderer.hasPopInstruction = true;
graphic.SetMaterialDirty();
}

MaskUtilities.NotifyStencilStateChanged(this);
}

protected override void OnDisable()
{
// we call base OnDisable first here
// as we need to have the IsActive return the
// correct value when we notify the children
// that the mask state has changed.
base.OnDisable();
if (graphic != null)
{
graphic.SetMaterialDirty();
graphic.canvasRenderer.hasPopInstruction = false;
graphic.canvasRenderer.popMaterialCount = 0;
}

StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = null;
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = null;

MaskUtilities.NotifyStencilStateChanged(this);
}

hasPopInstruction设置为true,表示其对应的Renderer(CanvasRenderer)插入pop指令,在所有的子节点被绘制完成后会执行pop指令。对于Mask来说就是在绘制时设置材质的模板参数,并在完成所有子节点的绘制之后重置模板参数。

MaskUtilities.NotifyStencilStateChanged会通知该Mask的所有子节点中的IMaskable来重新计算遮罩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

RecalculateMasking()的实现可以在MaskableGraphic中看到,不再赘述。

Raycast相关

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

修改材质

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
/// 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.LogError("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);
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;
}

首先是使用MaskUtilities的两个方法,FindRootSortOverrideCanvas获取顶层Canvas或overrideSorting的Canvas,GetStencilDepth传入之前获取到的Canvas并获取Mask所在结点的模板深度值,保存在stencilDepth中。如果深度大于8则输出错误并返回原材质。深度值做位运算得到desiredStencilBit,这个值是真正写入材质里边模板参考值。稍后回频繁地用到一个方法StencilMaterial.Add(...),传入一组参数,这一组参数会作为模板参数被加到材质中去,返回修改之后的材质,同时新加的材质会保存到StencilMaterial所管理的一个材质容器中。另有一个StencilMaterial.Remove方法,从StencilMaterial管理的材质中移除指定材质。

Mask中保存的有两个材质m_MaskMaterialm_UnmaskMaterial,前者会在调用GetModifiedMaterial时被返回,即是绘制Mask本身所使用的的材质,后者会被使用SetPopMaterial方法传给graphic.canvasRenderer,推断会在pop指令时使用。m_MaskMaterial对模板值的修改会影响到Mask所有的子结点的绘制(模板测试),而m_UnmaskMaterial则会恢复(重置)m_MaskMaterial及子结点元素对模板值的修改。

此处会根据是否是第一层的遮罩来走两个分支,分别设置m_MaskMaterialm_UnmaskMaterial。为便于对比,将四次调用StencilMaterial.Add(...)时模板参数的设置列出来放在下表中(表中用Bit指代desiredStencilBit)。为便于对比说明将MaskableGraphic对材质的修改操作也列入表内(第五行):

首层 材质 Stencil StencilOp Compare Function Stencil Read Mask Stencil Write Mask
mask 1 Replace Always 255 255
unmask 1 Zero Always 255 255
mask Bit \ (Bit - 1) Replace Equal Bit - 1 Bit \ (Bit - 1)
unmask Bit - 1 Replace Equal Bit - 1 Bit \ (Bit - 1)
- - Bit - 1 Keep Equal Bit - 1 0
  1. 首先是考虑首层Mask与非首层Mask的区别,首层级别的Mask对模板的读写不使用Mask,8位全部读/写,而对于非首层的Mask则借助读写Mask,读取时只读取低于stencilDepth的位的模板值,写入的时候只写入小于等于stencilDepth的位的模板值。首层的Mask使用比较函数是Always,非首层使用Equal,当通过测试后均会使用自己的模板值来取代模板缓冲中的模板值(首层Mask的Bit值为1,其对应的unmask材质使用非首层的unmask材质参数同样适用)。

  2. 较低层级的MaskstencilDepth更大)会覆盖掉较高层级的Mask对模板的操作,各层级的maskMaterial会把所在层级及更低层级(对应desiredStencilBit中的位)的模板值置为1,而各层级unmaskMaterial会将所在的层级(对应desiredStencilBit中的位)写成0

  3. 对于MaskableGraphic,与unmaskMaterial的模板参考值和比较函数相同,但是它只会使用模板值来进行测试,不会修改模板值,以此来实现图片的遮罩。


本系列其它文章详见Unity3D UGUI 源码学习

REFERENCE