1.0 UGUI Mask遮罩
UGUI為我們提供了2個(gè)遮罩組件,分別是RectMask2D和Mask。下面分別說(shuō)一下,這倆個(gè)遮罩的實(shí)現(xiàn)原理與區(qū)別。
2.0 RectMask2D組件
RectMask2D類(lèi)結(jié)構(gòu)如下圖,實(shí)現(xiàn)了IClipper與ICanvasRaycastFilter接口。

- 前面我們有介紹過(guò),當(dāng)UGUI需要重繪UI Mesh時(shí),會(huì)調(diào)用CanvasUpdateRegistry.PerformUpdate()方法,在該方法中會(huì)先更新Layout,然后調(diào)用 ClipperRegistry.instance.Cull();進(jìn)行裁剪,之后更新Render,進(jìn)行Mesh重繪。
CanvasUpdateRegistry.cs部分源碼如下:
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();
m_PerformingLayoutUpdate = true;
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
//省略代碼 更新layout
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
// now layout is complete do culling...
ClipperRegistry.instance.Cull();
m_PerformingGraphicUpdate = true;
//省略代碼 更新Render
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
- ClipperRegistry管理著所有的IClipper對(duì)象,在Layout更新之后,通過(guò)Cull()方法,遍歷IClipper的PerformClipping()方法,執(zhí)行具體的裁剪邏輯。
ClipperRegistry.cs部分源碼如下:
public class ClipperRegistry
{
readonly IndexedSet<IClipper> m_Clippers = new IndexedSet<IClipper>();
/// <summary>
/// Perform the clipping on all registered IClipper
/// </summary>
public void Cull()
{
for (var i = 0; i < m_Clippers.Count; ++i)
{
m_Clippers[i].PerformClipping();
}
}
public static void Register(IClipper c)
{
if (c == null)
return;
instance.m_Clippers.AddUnique(c);
}
public static void Unregister(IClipper c)
{
instance.m_Clippers.Remove(c);
}
}
- 我們的RectMask2D組件實(shí)現(xiàn)了IClipper接口,并且在OnEnable()注冊(cè)和OnDisable()注銷(xiāo)到ClipperRegistry的m_Clippers集合中。
RectMask2D.cs部分源碼如下:
public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
{
protected override void OnEnable()
{
base.OnEnable();
m_ShouldRecalculateClipRects = true;
ClipperRegistry.Register(this);
MaskUtilities.Notify2DMaskStateChanged(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();
m_ClipTargets.Clear();
m_Clippers.Clear();
ClipperRegistry.Unregister(this);
MaskUtilities.Notify2DMaskStateChanged(this);
}
}
- 同時(shí)調(diào)用MaskUtilities.Notify2DMaskStateChanged(this);通知該RectMask2D下的所有的IClippable(Text,Image,RawImage)遮罩狀態(tài)發(fā)生改變。
MaskUtilities提供了遮罩的具體處理邏輯,包括RectMask2D和Mask組件,這里先介紹RectMask2D相關(guān)代碼,后文再介紹Mask相關(guān)代碼。
MaskUtilities.cs部分源碼如下:
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);
}
- Text,Image,RawImage都繼承MaskableGraphic,MaskableGraphic實(shí)現(xiàn)了IClippable接口。
MaskUtilities.GetRectMaskForClippable(this)會(huì)獲取到該MaskableGraphic父級(jí)第一個(gè)有效的RectMask2D組件。 - 然后將該MaskableGraphic從之前的RectMask2D中剔除,并通過(guò)UpdateCull(false)加入待重建的隊(duì)列中,最后,將該MaskableGraphic添加到新的RectMask2D組件中。
- 除此之外MaskableGraphic的OnEnable(),OnDisable(),OnTransformParentChanged()都會(huì)調(diào)用UpdateClipParent()用于將該MaskableGraphic添加或者移除到對(duì)應(yīng)的RectMask2D中。
MaskableGraphic.cs部分源碼如下:
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
{
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;
}
/// <summary>
/// See IClippable.RecalculateClipping
/// </summary>
public virtual void RecalculateClipping()
{
UpdateClipParent();
}
protected override void OnEnable()
{
base.OnEnable();
m_ShouldRecalculateStencil = true;
UpdateClipParent();
SetMaterialDirty();
if (GetComponent<Mask>() != null)
{
MaskUtilities.NotifyStencilStateChanged(this);
}
}
protected override void OnDisable()
{
base.OnDisable();
m_ShouldRecalculateStencil = true;
SetMaterialDirty();
UpdateClipParent();
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = null;
if (GetComponent<Mask>() != null)
{
MaskUtilities.NotifyStencilStateChanged(this);
}
}
protected override void OnTransformParentChanged()
{
base.OnTransformParentChanged();
if (!isActiveAndEnabled)
return;
m_ShouldRecalculateStencil = true;
UpdateClipParent();
SetMaterialDirty();
}
}
- 之前我們有講過(guò)CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this)將UI添加到待重建的Graphic隊(duì)列中。
private void UpdateCull(bool cull)
{
if (canvasRenderer.cull != cull)
{
canvasRenderer.cull = cull;
UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
m_OnCullStateChanged.Invoke(cull);
OnCullingChanged();
}
}
public virtual void OnCullingChanged()
{
if (!canvasRenderer.cull && (m_VertsDirty || m_MaterialDirty))
{
/// When we were culled, we potentially skipped calls to <c>Rebuild</c>.
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
}
}
- RectMask2D中m_ClipTargets 管理著該遮罩下所有的待裁剪的IClippable對(duì)象。
RectMask2D.cs部分源碼如下:
public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
{
[NonSerialized]
private HashSet<IClippable> m_ClipTargets = new HashSet<IClippable>();
/// <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;
if (!m_ClipTargets.Contains(clippable))
m_ClipTargets.Add(clippable);
m_ForceClip = true;
}
/// <summary>
/// Remove an IClippable from being tracked by the mask.
/// </summary>
/// <param name="clippable">Remove the clippable object from this mask</param>
public void RemoveClippable(IClippable clippable)
{
if (clippable == null)
return;
m_ShouldRecalculateClipRects = true;
clippable.SetClipRect(new Rect(), false);
m_ClipTargets.Remove(clippable);
m_ForceClip = true;
}
}
- 在Remove是會(huì)clippable.SetClipRect(new Rect(), false)清除對(duì)該clippable的裁剪。canvasRenderer.EnableRectClipping(clipRect)對(duì)clipRect以外的區(qū)域進(jìn)行裁剪,canvasRenderer.DisableRectClipping();取消裁剪。
MaskableGraphic.cs部分源碼如下:
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
以上就是RectMask2D狀態(tài)改變時(shí),通知該RectMask2D下的所有的IClippable(Text,Image,RawImage)遮罩狀態(tài)發(fā)生改變的所有流程。
我們?cè)賮?lái)看看ClipperRegistry,在Layout更新之后,通過(guò)Cull()方法,遍歷IClipper的PerformClipping()方法,執(zhí)行的裁剪邏輯。
- MaskUtilities.GetRectMasksForClip(this, m_Clippers)會(huì)獲取該節(jié)點(diǎn)父級(jí)(含自身)所有的RectMask2D組件,保存到m_Clippers(為了處理RectMask2D組件嵌套的情況),相關(guān)代碼請(qǐng)自行查看源碼。
- Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);會(huì)計(jì)算出m_Clippers這些RectMask2D的最小可見(jiàn)區(qū)域,保存到validRect,相關(guān)代碼請(qǐng)自行查看源碼。
- 然后遍歷m_ClipTargets,將最小可見(jiàn)區(qū)域validRect設(shè)置給每一個(gè)IClippable對(duì)象,調(diào)用 clipTarget.SetClipRect(clipRect, validRect),上文也介紹了SetClipRect()用于設(shè)置裁剪區(qū)域。
RectMask2D.cs部分源碼如下:
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);
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);
bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
bool forceClip = m_ForceClip;
// Avoid looping multiple times.
foreach (IClippable clipTarget in m_ClipTargets)
{
if (clipRectChanged || forceClip)
{
clipTarget.SetClipRect(clipRect, validRect);
}
var maskable = clipTarget as MaskableGraphic;
if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
continue;
// 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.
clipTarget.Cull(
maskIsCulled ? Rect.zero : clipRect,
maskIsCulled ? false : validRect);
}
m_LastClipRectCanvasSpace = clipRect;
m_ForceClip = false;
}
- 并且調(diào)用clipTarget.Cull()方法,將UI添加到待重建的Graphic隊(duì)列中。UpdateCull()方法,上文有說(shuō)過(guò)。
public virtual void Cull(Rect clipRect, bool validRect)
{
var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
UpdateCull(cull);
}
- 下文,我們來(lái)看看canvasRenderer.EnableRectClipping(clipRect);為什么可以進(jìn)行裁剪處理,由于CanvasRenderer組件沒(méi)有開(kāi)源,我們需要使用Fram Debugger工具進(jìn)行查看。
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
-
打開(kāi)FrameDebug工具,點(diǎn)擊Enable,
沒(méi)有RectMask2D組件結(jié)果如圖:
沒(méi)有RectMask2D組件
FrameDebug 沒(méi)有RectMask2D組件
添加了RectMask2D組件結(jié)果如圖:
添加RectMask2D組件

- 對(duì)比可以發(fā)現(xiàn)UI默認(rèn)使用UI/Default Shader,使用RectMask2D會(huì)多一個(gè)DrawCall,沒(méi)有啟用Stencil測(cè)試(與Mask組件的區(qū)別),開(kāi)啟了一個(gè)UNITY_UI_CLIP_RECT的shader變體(可以理解為C#中的宏定義),_ClipRect為可顯示區(qū)域。
既然使用了UI/Default Shader,那我們就來(lái)看看該shader的源碼(請(qǐng)自行到官網(wǎng)下載),看一下UNITY_UI_CLIP_RECT的作用。
UI/Default Shader部分源碼如下:
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
可以看到啟動(dòng)UNITY_UI_CLIP_RECT,會(huì)執(zhí)行color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);來(lái)設(shè)置最終渲染的透明度
UnityGet2DClipping的作用,如果IN.worldPosition.xy在_ClipRect區(qū)域,則返回1,否則返回0,這樣就到達(dá)了_ClipRect區(qū)域可見(jiàn),_ClipRect以外的區(qū)域不可見(jiàn)的作用。
最終,我們可以確定RectMask2D是通過(guò)設(shè)置透明度的方式,讓某些區(qū)域不可見(jiàn)的。不過(guò),RectMask2D只能用于裁剪矩形區(qū)域。
3.0 Mask組件
Mask組件涉及到了shader的模板測(cè)試,相關(guān)內(nèi)容,可以看下這篇文章模板測(cè)試介紹,這里就不介紹了。
Mask類(lèi)結(jié)構(gòu)如下圖,實(shí)現(xiàn)了IMaterialModifier與ICanvasRaycastFilter接口。

- 之前在UI Mesh重建的文章中,講過(guò)當(dāng)UI的材質(zhì),貼圖變動(dòng)時(shí),會(huì)添加到CanvasUpdateRegistry中的m_GraphicRebuildQueue隊(duì)列中,當(dāng)Unity 執(zhí)行Canvas.willRenderCanvases事件時(shí),會(huì)觸發(fā)Graphic的UpdateMaterial()方法,進(jìn)行更新。
- materialForRendering是個(gè)屬性方法,會(huì)獲取該GameObject所有的IMaterialModifier對(duì)象,Mask與MaskableGraphic(Text,Image,RawImage)實(shí)現(xiàn)了該接口,調(diào)用對(duì)應(yīng)的重寫(xiě)方法GetModifiedMaterial()。
Graphic.cs部分源碼如下:
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}
public virtual Material materialForRendering
{
get
{
var components = ListPool<Component>.Get();
GetComponents(typeof(IMaterialModifier), components);
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<Component>.Release(components);
return currentMat;
}
}
- MaskUtilities.FindRootSortOverrideCanvas(transform)獲取該MaskableGraphic父級(jí)的Canvas組件,相關(guān)代碼請(qǐng)自行查看。
- MaskUtilities.GetStencilDepth(transform, rootCanvas) 獲取該MaskableGraphic相對(duì)于rootCanvas的深度值,相關(guān)代碼請(qǐng)自行查看。
MaskableGraphic.cs部分源碼如下:
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;
}
Mask.cs部分源碼如下:
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;
}
}
- StencilMaterial.Add()方法會(huì)根據(jù)傳進(jìn)的參數(shù),設(shè)置模板測(cè)試的數(shù)值。
- 同時(shí)開(kāi)啟UNITY_UI_ALPHACLIP
public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
{
//代碼省略...
var newEnt = new MatEntry();
newEnt.count = 1;
newEnt.baseMat = baseMat;
newEnt.customMat = new Material(baseMat);
newEnt.customMat.hideFlags = HideFlags.HideAndDontSave;
newEnt.stencilId = stencilID;
newEnt.operation = operation;
newEnt.compareFunction = compareFunction;
newEnt.readMask = readMask;
newEnt.writeMask = writeMask;
newEnt.colorMask = colorWriteMask;
newEnt.useAlphaClip = operation != StencilOp.Keep && writeMask > 0;
newEnt.customMat.name = string.Format("Stencil Id:{0}, Op:{1}, Comp:{2}, WriteMask:{3}, ReadMask:{4}, ColorMask:{5} AlphaClip:{6} ({7})", stencilID, operation, compareFunction, writeMask, readMask, colorWriteMask, newEnt.useAlphaClip, baseMat.name);
newEnt.customMat.SetInt("_Stencil", stencilID);
newEnt.customMat.SetInt("_StencilOp", (int)operation);
newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
newEnt.customMat.SetInt("_StencilReadMask", readMask);
newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
// left for backwards compatability
if (newEnt.customMat.HasProperty("_UseAlphaClip"))
newEnt.customMat.SetInt("_UseAlphaClip", newEnt.useAlphaClip ? 1 : 0);
if (newEnt.useAlphaClip)
newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
else
newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
m_List.Add(newEnt);
return newEnt.customMat;
}
- 依舊是UI/Default Shader文件,開(kāi)啟UNITY_UI_ALPHACLIP之后,會(huì)使用clip ()方法進(jìn)行裁剪
fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip (color.a - 0.001);
#endif
return color;
}
- 打開(kāi)FrameDebug工具,查看結(jié)果如下(這里為了說(shuō)明原理,只考慮有一個(gè)Mask的情況,嵌套情況原理是一樣的,只是邏輯比較復(fù)雜):
-
首先是繪制Mask所在的UI對(duì)象,開(kāi)啟UNITY_UI_ALPHACLIP,Stencil Ref為1,開(kāi)啟模板測(cè)試,Stencil Comp為Always,Stencil Pass為Replace,所以此時(shí)Mask所在的摸版檢測(cè)的緩沖中為1,其它區(qū)域?yàn)?。
第一個(gè)DrawCall 然后繪制Mask下需要被裁剪的UI對(duì)象,關(guān)閉UNITY_UI_ALPHACLIP,Stencil Ref為1,開(kāi)啟模板測(cè)試,Stencil Comp為Equal,所以只有與模板緩存相同的值(1)才能通過(guò)測(cè)試,也就是Mask遮罩所在的區(qū)域,因此Mask以外的區(qū)域不會(huì)顯示,Stencil Pass為Keep表示,不會(huì)改變模板緩存里的值。

3.最后,通過(guò)一個(gè)DrawCall,開(kāi)啟UNITY_UI_ALPHACLIP,Stencil Comp為Always,表示總是通過(guò)檢測(cè),Stencil Pass為0,表示將模板緩存里的值賦值為0,以此達(dá)到還原模板緩存的目的。

可看出來(lái),Mask組件使用了3個(gè)DrawCall,而且會(huì)破壞被裁剪的UI與其他UI的Mesh合并,因此盡量不要使用Mask組件。非要使用的話(huà),盡量使用RectMask2D代替Mask。
- 與RectMask2D相同,當(dāng)Mask組件改變時(shí),也會(huì)通知給它下面所有的IMaskable對(duì)象(MaskableGraphic實(shí)現(xiàn)了該接口)
Mask.cs部分源碼如下:
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);
}
- 遍歷所有的IMaskable,進(jìn)行RecalculateMasking()通知。
MaskUtilities.cs部分源碼如下:
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()方法中調(diào)用SetMaterialDirty(),將改UI添加到Graphic待重建隊(duì)列中。
MaskableGraphic.cs部分源碼如下:
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();
}
4.0 ICanvasRaycastFilter接口
需要注意的是:無(wú)論是使用Mask,還是RectMask2D組件,去裁剪一個(gè)Button,當(dāng)我們?nèi)c(diǎn)擊這個(gè)Button被裁剪的區(qū)域,其實(shí)這個(gè)Button是收到點(diǎn)擊事件的,但為什么沒(méi)有觸發(fā)對(duì)應(yīng)的OnClick事件呢?這個(gè)就是Mask和RectMask2D需要實(shí)現(xiàn)ICanvasRaycastFilter接口的原因。
我們知道,所有的Button的事件接受都是依靠Graphic對(duì)象的。下面是Graphic檢測(cè)時(shí)的代碼,可以看到這里并沒(méi)有對(duì)裁剪區(qū)域進(jìn)行判斷,那是怎么過(guò)濾掉裁剪區(qū)域點(diǎn)擊的呢?
Graphic.cs 部分源碼如下:
public virtual bool Raycast(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return false;
var t = transform;
var components = ListPool<Component>.Get();
bool ignoreParentGroups = false;
bool continueTraversal = true;
while (t != null)
{
t.GetComponents(components);
for (var i = 0; i < components.Count; i++)
{
var canvas = components[i] as Canvas;
if (canvas != null && canvas.overrideSorting)
continueTraversal = false;
var filter = components[i] as ICanvasRaycastFilter;
if (filter == null)
continue;
var raycastValid = true;
var group = components[i] as CanvasGroup;
if (group != null)
{
if (ignoreParentGroups == false && group.ignoreParentGroups)
{
ignoreParentGroups = true;
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else if (!ignoreParentGroups)
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
else
{
raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
}
if (!raycastValid)
{
ListPool<Component>.Release(components);
return false;
}
}
t = continueTraversal ? t.parent : null;
}
ListPool<Component>.Release(components);
return true;
}
原因在于一個(gè)Button點(diǎn)擊之后,還會(huì)遍歷它的所有父對(duì)象,并且獲取當(dāng)前遍歷的GameObject的ICanvasRaycastFilter,而Mask和RectMask2D實(shí)現(xiàn)了該接口,對(duì)應(yīng)的重寫(xiě)方法如下:
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return true;
return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
}
會(huì)判斷當(dāng)前點(diǎn)擊位置是否在改遮罩內(nèi)部,而我們點(diǎn)擊的是被裁剪的區(qū)域,自然不在遮罩內(nèi)部,所以就會(huì)返回false,進(jìn)而在Raycast()方法中過(guò)濾掉當(dāng)前點(diǎn)擊。



