UGUI源码解读——Mask和RectMask2D

4 篇文章 0 订阅
4 篇文章 0 订阅

两种遮罩共同实现了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的源码解读

UGUI源码解读——Image和RawImage

修改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后面做了优化,也可能是我的方法不对,如果有知道的可以评论区指导下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值