UGUI source code interpretation – Mask and RectMask2D

The two masks jointly implement ICanvasRacastFilter. We have seen this interface before in the Image class. It implements the judgment of whether Raycast is effective. The two masks are implemented in the same way. They both call the RectangleContainsScreenPoint method of the RectTransformUtility class. RectMask2D is a rectangle. area, so there is Padding to control the boundary offset, which has one more Vector4 parameter than Mask.

//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 implements IMaterialModifer. We remember that Image’s base class MaskableGraphic also implements this interface. Let’s first look at how to obtain the material that can be used by Mask. First, obtain the template value based on the Mask depth of the root Canvas. If it can be Masked, create a new material based on baseMaterial. During rendering, the value of the template buffer will be taken out to determine whether it is equal to 2^template depth-1. If so, it will be rendered. .

public virtual Material GetModifiedMaterial(Material baseMaterial)
{
    var toUse = baseMaterial;

    if (m_ShouldRecalculateStencil)
    {
        if (maskable)
        {
            //Get the root canvas
            var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
            //Get the template depth, that is, how many Mask components the parent object has
            m_StencilValue = MaskUtilities.GetStencilDepth(transform, rootCanvas);
        }
        else
            m_StencilValue = 0;

        m_ShouldRecalculateStencil = false;
    }

    if (m_StencilValue > 0 & amp; & amp; !isMaskingGraphic)
    {
        //Create a new material based on baseMaterial. During rendering, the value of the template buffer will be taken out to determine whether it is equal to 2^template depth-1. If so, it will be rendered.
        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;
}

How to deal with the value of the template buffer? Let’s look at the IMaterialModifer implemented by Mask. Also first obtain the template value based on the Mask depth of the root Canvas. Note that the search is upward from the Mask, so it will be one less layer than Maskable. When the Mask is the first layer below the Canvas, directly set the value of the template buffer to 1. unmaskMaterial is used to restore the template value to 0 after masking. The following code is compatible with the same idea when the Mask is not on the first layer.

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)
    {
        //Create a new material based on baseMaterial, which will set the value of the template buffer to 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;
}

For the meaning of several parameters of StencilMaterial.Add, please refer to the Unity Manual ShaderLab Command: Template – Unity Manual (unity3d.com)

Let’s take a look at how Mask triggers the calculation of the mask. When obtaining the materialForRendering property in the Graphic object, the GetModifiedMaterial method of the object that implements the IMaterialModifier interface will be called. This property is obtained when the material dirty mark is detected during the Rebuild process, so you only need to pay attention to the material dirty mark. It just matters when it is triggered. In Mask, RecalculateMasking is called on components that implement the IMaskable interface through the MaskUtilities.NotifyStencilStateChanged method. All objects that inherit the MaskableGraphic class will mark the material as dirty.

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

The way RectMask2D implements masking is different from Mask. Let’s take a look at RectMask2D. It first finds all RectMask components including itself through MaskUtilities.GetRectMasksForClip, and then uses Clipping.FindCullAndClipWorldRect to find the rectangle that overlaps all RectMask components, and then traverses the child nodes. And call the SetClipRect method, and finally the CanvasRenderer completes the cropping operation.

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) & amp; & amp;
        !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();
}

Let’s take a look at how this is triggered. RectMask2D implements the component RecalculateClipping of the IClippable interface through the MaskUtilities.Notify2DMaskStateChanged notification, so the object UpdateClipParent inherited from the MaskableGraphic class is registered in the grid reconstruction collection during the UpdateCull process, and is passed through Rebuild The ClipperRegistry.instance.Cull method triggers PerformClipping of all RectMask2D. If you don’t remember the Rebuild method, you can read the previous source code interpretation of Image.

UGUI source code interpretation – Image and RawImage

When modifying padding and softness, enable or disable will trigger re-cropping.

//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 & amp; & amp; (m_VertsDirty || m_MaterialDirty))
    {
        CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
    }
}

Finally, we focus on the performance differences between the two masks. The first is the test of two Masks

It can be seen that Masks can be batched together without overlapping. Let’s look at RectMask2D in the same situation.

You can see that no matter whether they overlap or not, they will not be batched together.

We can explore the reasons for the above two situations. Mask is easy to understand, because if two Masks overlap, their depths must be different, so they will not be batched together. Similarly, if two Masks do not overlap, their depths will not be the same. The same effect can be achieved by superimposing an Image underneath one of them.

In theory, those that do not overlap and have the same depth can be batched together. Why can’t RectMask2D do it? Let’s compare the Vectors panel of the FrameDebugger of two non-overlapping RectMask2Ds. We can see that the value of the material attribute _ClipRect is different, because it is determined based on the RectTransform value of RectMask2D, so as long as the two RectMask2Ds are not completely coincident, they The materials are definitely different, so they will not be batched together.

Here’s a little more extension. When I check the batching rules, I often see articles saying that when the z values of ui are different, batching will be interrupted. I tried it. Whether it is Canvas, Overlay or Camera, two z Whether pictures with different values overlap will not interrupt the batching. It may be that Unity has optimized it later, or it may be that my method is wrong. If you know anything, please provide guidance in the comment area.