Mask, IMaterialModifier and IMaskable of UGUI source code

Posted by Vincent III on Mon, 24 Jan 2022 03:09:53 +0100

1, The essence of Mask

1. Mask is masked through template testing.

Unity template test brief document.

You need to know that template testing occurs in the rasterization stage / piece by piece operation. As shown in the figure:

2. Mask enables the UI system to complete the template test by creating a mask material.

(1) Mask component sets the hasPopInstruction field of the CanvasRenderer associated with the same level to true when OnEnable(). Make all the child nodes of this node paint once more for masking.

(2) the Mask component will generate an unmaskMaterial for the CanvasRenderer associated with the same level in the GetModifiedMaterial method (implemented from # IMaterialModifier). And set to popMaterial for masking.

(3) the Mask component will generate a {maskMaterial for the Graphic component of the same level in the} GetModifiedMaterial method (implemented from} IMaterialModifier). (finally assigned to CanvasRenderer) (see {canvasRenderer.SetMaterial(materialForRendering, 0);)

(4) maskable Graphic, a child of Mask, will generate a maskMaterial for itself in GetModifiedMaterial method (implemented from # IMaterialModifier). (finally assigned to CanvasRenderer) (see {canvasRenderer.SetMaterial(materialForRendering, 0);)

(5) you can see from the StencilMaterial class that the generated materials are based on UI default The shader is created, and then its template test related parameters are modified. You can continue to view UI default Shader for details. (download built in shaders from Unity's official website: https://unity.cn/releases/lts . )

(6) in order to make multi-layer masks work nested (take the intersection as # RectMask2D), different template test parameters will be provided according to their own depth (where they are located and under the nested Mask) when generating these materials.

For example, a three-tier nesting example. (the parameter comparison process during specific test needs to be studied.)

The original picture is in my processon: todo

3. Simple comparison of Mask and RectMask2D in terms of performance.

(1) the template test of Mask occurs in the rasterization stage / slice by slice operation, while the coarse clipping of RectMask2D occurs in the application stage / setting the rendering state. It can be seen that the coarse clipping of RectMask2D is performed very early, and the rendering process of the clipped part can be greatly skipped.

(2) Mask will set hasPopInstruction of canvas renderer to true, which will add a drawcall.

(3) Mask is based on UI default The shader creates new materials for itself and its child graphics, so it will break the batch with other UIs.

2, Full notes:

----------------------NRatel cut-------------------------------

More UGUI comments have been placed https://github.com/NRatel/uGUI.

----------------------NRatel cut-------------------------------

1,IMaterialModifier

namespace UnityEngine.UI
{
    // Interface which allows for the modification of the Material used to render a Graphic before they are passed to the CanvasRenderer.
    // When a Graphic sets a material is is passed (in order) to any components on the GameObject that implement IMaterialModifier.
    // This component can modify the material to be used for rendering.
    // This interface allows the material that renders a graphic to be modified before being transferred to the canvas renderer.
    public interface IMaterialModifier
    {
        // Perform material modification in this function.
        // In this method, you make changes to the material.
        // "baseMaterial": The material that is to be modified.  // Material to be modified
        // Return value: The modified material// Modified material
        Material GetModifiedMaterial(Material baseMaterial);
    }
}

2,Mask

using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.Serialization;

namespace UnityEngine.UI
{
    [AddComponentMenu("UI/Mask", 13)]
    [ExecuteAlways]
    [RequireComponent(typeof(RectTransform))]
    [DisallowMultipleComponent]
    // A component for masking children elements.
    // By using this element any children elements that have masking enabled will mask where a sibling Graphic would write 0 to the stencil buffer.
    // A component used to mask sub elements.
    // By using this element, any child element with Mask (maskable of maskable Graphic) enabled will be masked. The Graphic associated with the same level of this component will write 0 on the template test buffer.
    public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
    {
        [NonSerialized]
        private RectTransform m_RectTransform;  //RectTransform associated with this component.
        public RectTransform rectTransform
        {
            get { return m_RectTransform ?? (m_RectTransform = GetComponent<RectTransform>()); }
        }

        [SerializeField]
        private bool m_ShowMaskGraphic = true;

        // Show the graphic that is associated with the Mask render area.
        // Whether to display the rendered area of the graph associated with the Mask.
        // When setting, if the associated Graphic is not null, the material marking Graphic is dirty and marked as dirty.
        public bool showMaskGraphic
        {
            get { return m_ShowMaskGraphic; }
            set
            {
                if (m_ShowMaskGraphic == value)
                    return;

                m_ShowMaskGraphic = value;
                if (graphic != null)
                    graphic.SetMaterialDirty();
            }
        }

        [NonSerialized]
        private Graphic m_Graphic;

        // The graphic associated with the Mask.
        // graphic associated with Mask.
        public Graphic graphic
        {
            get { return m_Graphic ?? (m_Graphic = GetComponent<Graphic>()); }
        }
          
        [NonSerialized]
        private Material m_MaskMaterial;    //Mask material

        [NonSerialized]
        private Material m_UnmaskMaterial;

        protected Mask()
        {}

        //Mask enabled (effective): activated and the associated Graphic is not null.
        public virtual bool MaskEnabled() { return IsActive() && graphic != null; }

        [Obsolete("Not used anymore.")]
        public virtual void OnSiblingGraphicEnabledDisabled() {}

        // 1. Call the parent class OnEnable.
        // 2. If the associated Graphic is not null
        //    (1) enable hasPopInstruction of canvas renderer component associated with Graphic.
        //    ⑵ mark the material of Graphic as dirty.
        // 3. Notify StencilStateChanged. (notify all sub objects that implement the IMaskable interface to recalculate the mask.
        protected override void OnEnable()
        {
            base.OnEnable();
            if (graphic != null)
            {
                // hasPopInstruction: 
                //    Enable"render stack"pop draw call. 
                //    When rendering with hierarchy, canvasRenderer can insert a "pop" instruction.
                //    This "pop" instruction will be executed after all child elements are rendered.
                //    The canvas renderer component renders using the configured pop material.
                graphic.canvasRenderer.hasPopInstruction = true;    
                graphic.SetMaterialDirty();
            }

            MaskUtilities.NotifyStencilStateChanged(this);
        }

        // 1. Call the parent class OnDisable.
        // 2. If the associated Graphic is not null
        //    (1) mark the material of Graphic as dirty.
        //    (2) close the hasPopInstruction of the canvas renderer component associated with Graphic.
        //    (3) set popMaterialCount of canvas renderer component associated with Graphic to 0.
        // 3. Will m_MaskMaterial is removed from stencilmeterial and M is set_ Maskmaterial is null.
        // 4. Will m_UnmaskMaterial is removed from StencilMaterial and set m_UnmaskMaterial is null.
        // 5. Notify StencilStateChanged. (notify all sub objects that implement the IMaskable interface to recalculate the mask.
        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.
            // First, we call base Ondisable, because we need to let IsActive return the correct value when notifying the child object of Mask state change.
            // doubt??? I don't understand. Why do you adjust base Ondisable will affect IsActive.
            // In actual testing, call the base. in the OnDisable of the subclass of a UIBehaviour. Before and after ondisable, both activeInHierarchy and activeSelf are false.
            base.OnDisable();
            if (graphic != null)
            {
                graphic.SetMaterialDirty();
                graphic.canvasRenderer.hasPopInstruction = false;
                graphic.canvasRenderer.popMaterialCount = 0;    // popMaterialCount: the number of materials available to the canvas renderer component for internal masking.
            }

            StencilMaterial.Remove(m_MaskMaterial);
            m_MaskMaterial = null;
            StencilMaterial.Remove(m_UnmaskMaterial);
            m_UnmaskMaterial = null;

            MaskUtilities.NotifyStencilStateChanged(this);
        }

#if UNITY_EDITOR
        protected override void OnValidate()
        {
            base.OnValidate();

            if (!IsActive())
                return;

            if (graphic != null)
                graphic.SetMaterialDirty();

            MaskUtilities.NotifyStencilStateChanged(this);
        }

#endif
        // Implement the interface of ICanvasRaycastFilter
        // Is the ray projection position valid
        public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
        {
            if (!isActiveAndEnabled)     //Valid if not activated or enabled (no filtering)
                return true;

            // If activated and enabled, check whether the projection point is within the rectangle of this rectTransform. Valid in.
            return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
        }

        // Stencil calculation time!
        // The actual template test is carried out here!
        // Implement the interface of IMaterialModifier.
        // 1. Check whether Mask is enabled. If not, return to baseMaterial directly.
        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
            if (!MaskEnabled())
                return baseMaterial;

            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);   // Gets the Canvas with the deepest root, or the first Canvas with "use independent draw order".
            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); // Calculate the template test depth.
            if (stencilDepth >= 8)
            {
                Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
                return baseMaterial;    //If the depth is > = 8, a warning is thrown and the baseMaterial is returned directly.
            }

            int desiredStencilBit = 1 << stencilDepth;  //Expected template test depth Bit.

            // if we are at the first level... we want to destroy what is there
            // If it is the first layer of nested Mask (the top layer) 
            if (desiredStencilBit == 1)  // (i.e. stencilDepth == 0).
            {
                // Create / get the material used by the Gaphic associated with the first layer Mask: m_MaskMaterial
                var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
                StencilMaterial.Remove(m_MaskMaterial); 
                m_MaskMaterial = maskMaterial;

                // Create / get pop material used by Gaphic associated with the first layer Mask: unmaskMaterial
                var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
                StencilMaterial.Remove(m_UnmaskMaterial); 
                m_UnmaskMaterial = unmaskMaterial;

                graphic.canvasRenderer.popMaterialCount = 1;     // popMaterialCount: the number of materials available to the canvas renderer component for internal masking.
                graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);  //SetPopMaterial: sets the material of the canvasRenderer for the internal mask.

                return m_MaskMaterial;  //Returns the modified material
            }

            //otherwise we need to be a bit smarter and set some read / write masks
            // Otherwise. You need to set some read / write masks.

            // Create / get pop material used by Gaphic associated with layer N Mask: 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;

            // Create / get pop material used by Gaphic associated with Mask of layer N: unmaskMaterial
            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;     // popMaterialCount: the number of materials available to the canvas renderer component for internal masking.
            graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); //SetPopMaterial: sets the material of the canvasRenderer for the internal mask.

            return m_MaskMaterial;  //Returns the modified material
        }
    }
}

3,IMaskable

When the Mask state changes, MaskUtilities uses this interface to update the Mask.

using System;

namespace UnityEngine.UI
{
    // This element is capable of being masked out.
    // This element can be masked. (currently only maskable graphic implements it)
    public interface IMaskable
    {
        // Recalculate masking for this element and all children elements.
        // Use this to update the internal state (recreate materials etc).
        // Recalculate the mask for this element and all child elements.
        // Update the internal state (recreate materials, etc.).
        void RecalculateMasking();
    }
}

Topics: UGUI