两种遮罩共同实现了ICanvasRacastFilter,这个接口我们之前在Image类中见过,它实现了判断Raycast是否生效,两种遮罩实现方式一样,都是调用了RectTransformUtility类的RectangleContainsScreenPoint方法,RectMask2D因为是矩形区域,所以存在Padding控制边界偏移,比Mask多了一个Vector4参数。
//Mask
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return true;
return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
}
//RectMask2D
public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
{
if (!isActiveAndEnabled)
return true;
return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera, m_Padding);
}
Mask实现IMaterialModifer,我们记得Image的基类MaskableGraphic也实现了这个接口,我们首先看下可以被Mask的材质如何获取。首先根据根Canvas的Mask深度获得模板值,如果可以被Mask的话,创建一个基于baseMaterial的新材质,在渲染时会取出模板缓冲区的值判断是否等于2^模板深度-1,如果是才会渲染。
public virtual Material GetModifiedMaterial(Material baseMaterial)
{
var toUse = baseMaterial;
if (m_ShouldRecalculateStencil)
{
if (maskable)
{
//获取根canvas
var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
//获取模板深度,即父物体有多少Mask组件
m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
}
else
m_StencilValue = 0;
m_ShouldRecalculateStencil = false;
}
if (m_StencilValue > 0 && !isMaskingGraphic)
{
//创建一个基于baseMaterial的新材质,在渲染时会取出模板缓冲区的值判断是否等于2^模板深度-1,如果是才会渲染
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实现的IMaterialModifer。同样先根据根Canvas的Mask深度获得模板值,注意这里是由Mask向上寻找的,所以会比Maskable的少一层,当这个Mask是Canvas下面第一层时,直接将模板缓冲区的值置为1。unmaskMaterial用于遮罩结束恢复模板值为0,后面的代码以同样的思路兼容Mask不在第一层的情况。
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 (desiredStencilBit == 1)
{
//创建一个基于baseMaterial的新材质,会将模板缓冲区的值置为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;
}
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的几个参数的含义可以参考Unity手册ShaderLab 命令:模板 - Unity 手册 (unity3d.com)
我们看一下Mask怎么触发计算遮罩,Graphic对象中获取materialForRendering属性时会调用实现IMaterialModifier接口的对象的GetModifiedMaterial方法,这个属性是在Rebuild过程中检测到材质脏标记时获取,所以只需要关注材质脏标记什么时候触发就可以了。在Mask中,是通过MaskUtilities.NotifyStencilStateChanged方法,对实现了IMaskable接口的组件调用RecalculateMasking,所有继承了MaskableGraphic类的对象会将材质脏标记。
//Graphic
public virtual Material materialForRendering
{
get
{
var components = ListPool<IMaterialModifier>.Get();
GetComponents<IMaterialModifier>(components);
var currentMat = material;
for (var i = 0; i < components.Count; i++)
currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
ListPool<IMaterialModifier>.Release(components);
return currentMat;
}
}
//Mask
protected override void OnEnable()
{
base.OnEnable();
MaskUtilities.NotifyStencilStateChanged(this);
}
protected override void OnDisable()
{
base.OnDisable();
MaskUtilities.NotifyStencilStateChanged(this);
}
//MaskUtilities
public static void NotifyStencilStateChanged(Component mask)
{
//...
for (var i = 0; i < components.Count; i++)
{
var toNotify = components[i] as IMaskable;
if (toNotify != null)
toNotify.RecalculateMasking();
}
}
//MaskableGraphic
public virtual void RecalculateMasking()
{
//...
SetMaterialDirty();
}
RectMask2D实现遮罩的方式和Mask不太一样,我们看下RectMask2D,它先通过MaskUtilities.GetRectMasksForClip找到包括自身的所有RectMask组件,再通过Clipping.FindCullAndClipWorldRect找到与所有RectMask组件都重叠的矩形,然后遍历子节点并调用SetClipRect方法,最后由CanvasRenderer完成裁剪操作。
public virtual void PerformClipping()
{
if (ReferenceEquals(Canvas, null))
{
return;
}
if (m_ShouldRecalculateClipRects)
{
MaskUtilities.GetRectMasksForClip(this, m_Clippers);
m_ShouldRecalculateClipRects = false;
}
bool validRect = true;
Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
RenderMode renderMode = Canvas.rootCanvas.renderMode;
bool maskIsCulled =
(renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
!clipRect.Overlaps(rootCanvasRect, true);
if (maskIsCulled)
{
clipRect = Rect.zero;
validRect = false;
}
if (clipRect != m_LastClipRectCanvasSpace)
{
foreach (IClippable clipTarget in m_ClipTargets)
{
clipTarget.SetClipRect(clipRect, validRect);
}
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)
{
maskableTarget.Cull(clipRect, validRect);
}
}
m_LastClipRectCanvasSpace = clipRect;
m_ForceClip = false;
UpdateClipSoftness();
}
public virtual void SetClipRect(Rect clipRect, bool validRect)
{
if (validRect)
canvasRenderer.EnableRectClipping(clipRect);
else
canvasRenderer.DisableRectClipping();
}
我们接下来看下这个是如何触发的,RectMask2D通过MaskUtilities.Notify2DMaskStateChanged通知实现了IClippable接口的组件RecalculateClipping,所以继承自MaskableGraphic类的对象UpdateClipParent,在UpdateCull的过程中注册进网格重建的集合中,通过Rebuild的ClipperRegistry.instance.Cull方法触发所有RectMask2D的PerformClipping,如果不记得Rebuild方法的可以看下之前Image的源码解读
修改padding、softness时,enable、disable时都会触发重新裁剪
//RectMask2D
public Vector4 padding
{
set
{
MaskUtilities.Notify2DMaskStateChanged(this);
}
}
public Vector2Int softness
{
set
{
MaskUtilities.Notify2DMaskStateChanged(this);
}
}
protected override void OnEnable()
{
base.OnEnable();
m_ShouldRecalculateClipRects = true;
ClipperRegistry.Register(this);
MaskUtilities.Notify2DMaskStateChanged(this);
}
protected override void OnDisable()
{
base.OnDisable();
ClipperRegistry.Disable(this);
MaskUtilities.Notify2DMaskStateChanged(this);
}
//MaskUtilities
public static void Notify2DMaskStateChanged(Component mask)
{
//...
for (var i = 0; i < components.Count; i++)
{
var toNotify = components[i] as IClippable;
if (toNotify != null)
toNotify.RecalculateClipping();
}
}
//MaskableGraphic
public virtual void RecalculateClipping()
{
UpdateClipParent();
}
private void UpdateClipParent()
{
//...
{
UpdateCull(false);
}
}
private void UpdateCull(bool cull)
{
if (canvasRenderer.cull != cull)
{
//...
OnCullingChanged();
}
}
public virtual void OnCullingChanged()
{
if (!canvasRenderer.cull && (m_VertsDirty || m_MaterialDirty))
{
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
}
}
最后我们关注一下两种遮罩在性能上的区别。首先是两个Mask的测试
可以看出Mask在不重叠的情况下是可以合批的,同样的情况我们再看下RectMask2D
可以看到无论是否重叠都不会合批。
我们可以探究一下上面两种情况的原因,Mask的很好理解,因为两个Mask如果重叠的话,他们的depth就一定不相同,所以就不会合批,同样的,在不重叠的两个Mask的其中一个下面叠加一个Image也能达到同样的效果。
那理论上不重叠的,depth相同的就可以合批,为什么RectMask2D不可以呢?我们再对比两个不重叠的RectMask2D的FrameDebugger的Vectors面板,可以看到_ClipRect这个材质属性的值是不同的,因为它是根据RectMask2D的RectTransform值决定的,所以只要两个RectMask2D不是完全重合,它们的材质是一定不相同的,所以也就不会合批。
这里再延申一点,我在查阅合批规则时候经常会看到有文章说当ui的z值不相同时会打断合批,我尝试了一下,无论是Canvas是Overlay还是Camera,两张z值不同的图片是否重叠,都不会打断合批,可能是Unity后面做了优化,也可能是我的方法不对,如果有知道的可以评论区指导下。