Unity UI - MaskableGraphic 剖析

前言

MaskableGraphic 是一种可以用来被遮罩的 Graphic。在 Unity UI 中,Text、Image 和 RawImage 等组件类都继承自 MaskableGraphic。

MaskableGraphic 类

MaskableGraphic 是一个抽象类,它继承自 Graphic 类;同时还实现了如下多个接口用于实现遮罩效果:

  • IClippable,用于 RectMask2D 实现遮罩时裁剪遮罩区域;

  • IMaskable,用来在使用 Mask 实现遮罩效果时标记当前 MaskableGraphic 是可被绘制遮罩的;

  • IMaterialModifier,用来在使用 Mask 实现遮罩效果时更新渲染材质 Shader 中的模板测试参数。

成员属性

属性 描述
m_MaskMaterial 使用 Mask 实现遮罩效果时渲染所用材质
m_ParentMask 使用 RectMask2D 实现遮罩效果时最适合的 RectMask2D
m_Maskable 当前 MaskableGraphic 是否允许被用来遮罩绘制
m_OnCullStateChanged 当 culling state 发生变化时回调事件
m_StencilValue 使用 Mask 实现遮罩效果时当前 MaskableGraphic 所在的模板测试深度
m_ShouldRecalculateStencil 标记是否需要重新计算模板测试数据

方法

GetModifiedMaterial 方法

这个方法是 IMaterialModifier 接口中定义的一个方法,它能够更新当前渲染所用的材质 Shader,在下次 CanvasRender 渲染的时候就能使用被修改之后的材质 Shader。使用 Mask 实现遮罩效果时,需要使用这个方法来修改材质 Shader。下面就来看看具体的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
// 1. calculate stencil ...
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
{
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
m_ShouldRecalculateStencil = false;
}
// 2. update mask material ...
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;
}

上面的代码主要分为两部分:

  1. 第一部分判断若需要计算模板测试深度 m_StencilValue,则调用 MaskUtilities 类的 GetStencilDepth 方法计算模板测试深度(MaskUtilities 类是遮罩相关的一个工具类,里面提供了很多遮罩绘制的辅助方法);

  2. 第二部分通过判断模板测试深度 m_StencilValue 大于 0 且当前元素不是一个 Mask(或者是未激活的 Mask),那么就更新材质 Shader;这里更新主要是调用 StencilMaterial 类的 Add 方法进行,主要设置了 Shader 中模板测试相关的参数,如模板测试参考值 Ref、模板测试参考值和模板缓冲值比较函数 Comp 等(StencilMaterial 类是一个管理 Mask 渲染所用材质的类,它里面也提供了一系列用来更新/创建 Mask 所需材质或清除 Mask 材质缓存的方法)。

GetModifiedMaterial 方法串联到整个渲染中再来看看整个流程。当 Graphic 重新构建时,若需要更新材质则会在当前使用材质基础上,获取当前 UI 元素下所有的 IMaterialModifier,并调用 GetModifiedMaterial 方法修改渲染所用材质;通过这些流程,相关模板测试参数就能够被写入 Shader 中,最终参与到遮罩效果的实现过程中。

UpdateCull 方法

首先来看看代码,如下:

1
2
3
4
5
6
7
8
9
10
private void UpdateCull(bool cull)
{
if (canvasRenderer.cull != cull)
{
canvasRenderer.cull = cull;
UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
m_OnCullStateChanged.Invoke(cull);
OnCullingChanged();
}
}

这个方法的作用主要就是更新当前 MaskableGraphic 所在的剔除 Cull 状态。当传入值为 true,那么当前所在的 CanvasRender 的 cull 属性也会被设置为 true,此时渲染时会忽略这个 CanvasRender 所在的 UI 元素(即当前 MaskableGraphic),同时相关事件回调被触发,最后调用 Graphic 类的 OnCullingChanged 方法标记当前 Graphic 重新构建(注意: 这里重新构建只有当剔除状态是 false 也就是不剔除是才会发生,因为只有不剔除才需要重新构建)。

RecalculateMasking 方法

这个方法是 IMaskable 接口中定义的,用来重置 Mask 渲染材质的一些值。首先它会从 StencilMaterial 缓存中清除先前渲染所用的材质,然后重置 m_ShouldRecalculateStenciltrue 表明下次渲染前需要重新计算模板测试相关参数,然后调用父类 Graphic 的 SetMaterialDirty 方法标记材质需要被重新构建。

RecalculateMasking 方法在 MaskUtilities 类的 NotifyStencilStateChanged 方法中被回调,而 NotifyStencilStateChanged 方法会在 Mask/MaskableGraphic 类的 OnEnableOnDisable 等方法被调用,因此在 Unity 生命周期的一些回调方法中,若可以则当前 MaskableGraphic 的遮罩渲染材质相关数值会被重置,以用来在下次重新构建 Graphic 之前重新计算渲染材质。

RecalculateClipping 方法

这个方法是 IClippable 接口定义的一个方法,用来重新计算相关裁剪信息。在 MaskableGraphic 类中该方法最终调用了 UpdateClipParent 方法,下面就来看看这个方法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void UpdateClipParent()
{
var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null;
if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
{
m_ParentMask.RemoveClippable(this);
UpdateCull(false);
}
if (newParent != null && newParent.IsActive())
newParent.AddClippable(this);
m_ParentMask = newParent;
}

上面的代码中,首先调用 MaskUtilities 类的 GetRectMaskForClippable 方法获取到当前 MaskableGraphic 最合适的 RectMask2D;若需要更新当前 m_ParentMask,则从之前的 RectMask2D 移除对自身的跟踪(调用了 RectMask2D 的 RemoveClippable 方法),紧接着调用自身的 UpdateCull 方法更新剔除 Cull 状态为 false(重置当前 MaskableGraphic 为不需要被剔除状态);然后将自身添加到新的 RectMask2D 跟踪列表中。

RecalculateClipping 方法在 MaskUtilities 类的 Notify2DMaskStateChanged 方法中被调用,而 Notify2DMaskStateChanged 方法在 RectMask2D 的 OnEnableOnDisable 等方法中被调用,也就是说在 RectMask2D 的生命周期回调方法中,通过调用 Notify2DMaskStateChanged 方法通知其自身以及子元素所有的 IClippable 更新遮罩计算中裁剪需要的相关信息,这也是 RectMask2D 实现遮罩效果比较重要的一步,只有这样 RectMask2D 才能跟踪到需要实施遮罩的子元素以在后面为其计算遮罩区域。

Cull 方法

同样也是 IClippable 接口中定义的一个方法,在 MaskableGraphic 类中最终也调用 UpdateCull 方法。代码如下:

1
2
3
4
5
public virtual void Cull(Rect clipRect, bool validRect)
{
var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
UpdateCull(cull);
}

在分析上面的代码之前,来看看这个方法被执行的过程。在 Canvas 重新构建 Layout 之后,Graphic 重建之前,剔除 Cull 被至此那个(调用了 ClipperRegistry 类的 Cull 方法);剔除过程中所有的 IClipper 的 PerformClipping 方法被调用(包括 RectMask2D);在 RectMask2D 的 PerformClipping 方法中计算实现遮罩效果所要裁剪区域 clipRect 等信息后,对于 MaskableGraphic 其 Cull 方法会被调用,到这里就来到了上面的代码部分。

上面的代码中如果所要裁剪区域 clipRect 不是一个合法的矩形区域或者这个区域和当前 MaskableGraphic 所在的根 Canvas 没有重叠,那么剔除 Cull 状态为 true,然后调用 UpdateCull 方法更新剔除状态。

SetClipRect 方法

仍然是 IClippable 接口中定义的方法,用来设置实现矩形遮罩效果时的遮罩区域。代码也很简单,就是调用当前所在 CanvasRender 的 EnableRectClippingDisableRectClipping 启用或禁用矩形遮罩,如下:

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

这个方法的调用过程和上面分析的 Cull 方法类似,在 RectMask2D 的 PerformClipping 方法中计算实现遮罩效果所要裁剪区域 clipRect 等信息后,SetClipRect 方法就会被调用来设置裁剪区域信息;除此之外这个方法还会在 RectMask2D 的 RemoveClippable 方法中被调用,当从一个 RectMask2D 移除当前 MaskableGraphic 时,调用 SetClipRect 禁用自身裁剪。

OnEnable、OnDisable 等生命周期回调方法

对于 MaskableGraphic 会进行一些数据的重置(清空)工作。

参考