UGUI 源码之 Mask、IMaterialModifier、IMaskable

原图放在我的 Processon:UGUI Mask+MaskableGraphic 工作原理| ProcessOn免费在线作图,在线流程图,在线思维导图

一、Mask 的实质

1、Mask 是通过模板测试进行遮罩的。

 ShaderLab 命令:模板 - Unity 手册

需要知道:模板测试发生在光栅化阶段/逐片元操作时。如图:

2、Mask 通过创建遮罩材质使UI系统完成模板测试。

⑴、Mask 组件在 OnEnable() 时将同级关联的 CanvasRenderer 的 hasPopInstruction 字段置为 true。使这个节点的所有子节点绘制完之后多一次绘制,用于遮罩。

⑵、Mask 组件会在 GetModifiedMaterial 方法(实现自 IMaterialModifier)中为同级关联的 CanvasRenderer 生成一个 unmaskMaterial。并设置为 popMaterial,用于遮罩。

⑶、Mask 组件会在 GetModifiedMaterial 方法(实现自 IMaterialModifier)中为同级的 Graphic 组件生成一个 maskMaterial。(最终赋给 CanvasRenderer)(见 Graphic类中的  canvasRenderer.SetMaterial(materialForRendering, 0);)

⑷、Mask 的子 MaskableGraphic 会在GetModifiedMaterial 方法(实现自 IMaterialModifier)中为自身生成一个maskMaterial。(最终赋给 CanvasRenderer)(见 Graphic类中的  canvasRenderer.SetMaterial(materialForRendering, 0);)

⑸、从 StencilMaterial 类中可以看到,生成的材质都是基于 UI-Default.shader 创建的,然后修改了其模板测试相关的参数。可以继续查看 UI-Default.shader 了解详情。(Unity 官网下载 Built-in-Shaders:https://unity.cn/releases/lts。)

⑹、为使多层 Mask 可以嵌套工作(和 RectMask2D 一样取交集),在生成这些材质时会根据自身深度(自身所在的、嵌套Mask下的位置)提供不同的模板测试参数。

例如一个三层嵌套的例子。

原图在我的 processon: UGUI Mask嵌套原理(3层示例)| ProcessOn免费在线作图,在线流程图,在线思维导图

3、模板测试参数

创建一个材质(如上图),Shader选择为UI/Default,关注这些参数:

⑴、StencilComp(_StencilComp):这个参数定义了在绘制UI元素时如何与模板缓冲进行比较。它控制着当前像素与模板缓冲中相应像素值的比较方式。其值对应枚举:

⑵、StencilID(_Stencil): 这个参数定义了在绘制UI元素时将要写入模板缓冲的值。它通常用于给UI元素设置一个唯一的标识值,以便在后续绘制中进行识别和控制。

⑶、StencilOp(_StencilOp):这个参数定义了在绘制UI元素时如何操作模板缓冲。它控制着模板缓冲中像素值的更新方式。其值对应枚举:

 ⑷、StencilWriteMask(_StencilWriteMask):这个参数定义了在绘制UI元素时哪些位可以被写入模板缓冲。它使用一个位掩码来指定允许写入的位。

⑸、StencilReadMask(_StencilReadMask):这个参数定义了在绘制UI元素时哪些位可以被读取模板缓冲。它使用一个位掩码来指定允许读取的位。

⑹、ColorMask(_ColorMask):这个参数定义了在绘制UI元素时哪些颜色通道会被写入帧缓冲(Frame Buffer)。它使用一个位掩码来指定允许写入的通道,包括红色(R)、绿色(G)、蓝色(B)、透明度(A)。其值对应枚举:

 ⑺、UseAlphaClip(_UseAlphaClip):这个参数用于启用或禁用Alpha裁剪(Alpha Clipping)效果。当启用Alpha裁剪时,UI元素会根据_AlphaClipThreshold参数的阈值来进行裁剪,使得UI元素的透明区域不会显示。

可再参考Unity文档中对参数的描述:ShaderLab 命令:模板 - Unity 手册

4、从性能上简单比较 Mask 和 RectMask2D。

⑴、Mask 的模板测试发生在光栅化阶段/逐片元操作时,而 RectMask2D 的粗裁剪发生在应用阶段/设置渲染状态时。可以看出 RectMask2D 的粗裁剪执行得很早,可以大幅略过被裁剪部分的渲染过程。

⑵、Mask 会将 CanvasRenderer的 hasPopInstruction 设为 true,这会增加一次 drawcall。

⑶、Mask 基于 UI-Default.shader 为自身及子Graphic 创建了新材质,所以会打断与其他UI的合批。

二、全注释: 

---------------------- NRatel 割 -------------------------------

更多 UGUI 注释已放入  https://github.com/NRatel/uGUI

---------------------- NRatel 割 -------------------------------

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.
    // 这个接口允许渲染一个图形的材质在传递到 CanvasRenderer 之前被修改。
    public interface IMaterialModifier
    {
        // Perform material modification in this function.
        // 在此方法中执行材质的修改。
        // "baseMaterial":The material that is to be modified.  //将被修改的材质
        // 返回值:The 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.
    // 用于遮罩子元素的组件。
    // 通过使用这个元素,任何启用了 Mask(MaskableGraphic 的 maskable) 的子元素都会被遮罩。与本组件同级关联的 Graphic 将在模板测试缓冲区上写入0。
    public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier
    {
        [NonSerialized]
        private RectTransform m_RectTransform;  //与本组件关联的 RectTransform。
        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.
        // 是否显示与 Mask 关联的图形的渲染区域。
        // 设置时,若关联的 Graphic 不为 null,则标记 Graphic 的材质脏标记 为脏。
        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.
        // 与 Mask 关联的 graphic。
        public Graphic graphic
        {
            get { return m_Graphic ?? (m_Graphic = GetComponent<Graphic>()); }
        }
          
        [NonSerialized]
        private Material m_MaskMaterial;    //Mask材质

        [NonSerialized]
        private Material m_UnmaskMaterial;

        protected Mask()
        {}

        //Mask是否启用(生效):激活且关联的 Graphic 不为 null。
        public virtual bool MaskEnabled() { return IsActive() && graphic != null; }

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

        // 1、调用父类 OnEnable。
        // 2、若关联的 Graphic 不为null
        //    ⑴、启用与 Graphic 关联的 CanvasRenderer 组件的 hasPopInstruction。
        //    ⑵、标记 Graphic 的材质脏标记 为脏。
        // 3、通知 StencilStateChanged。(通知所有实现 IMaskable 接口的子物体重新计算遮罩。
        protected override void OnEnable()
        {
            base.OnEnable();
            if (graphic != null)
            {
                // hasPopInstruction:
                //    Enable“render stack”pop draw call。
                //    当使用 hierarchy 渲染时,canvasRenderer 可以插入一个"pop"指令。
                //    这个"pop"指令将在所有子元素被渲染后执行。
                //    CanvasRenderer 组件将使用配置的 pop 材质渲染。
                graphic.canvasRenderer.hasPopInstruction = true;    
                graphic.SetMaterialDirty();
            }

            MaskUtilities.NotifyStencilStateChanged(this);
        }

        // 1、调用父类 OnDisable。
        // 2、若关联的 Graphic 不为null
        //    ⑴、标记 Graphic 的材质脏标记 为脏。
        //    ⑵、关闭与 Graphic 关联的 CanvasRenderer 组件的 hasPopInstruction。
        //    ⑶、设置与 Graphic 关联的 CanvasRenderer 组件的 popMaterialCount 为 0。
        // 3、将 m_MaskMaterial 从 StencilMaterial 中移除,并设置 m_MaskMaterial 为 null。
        // 4、将 m_UnmaskMaterial 从 StencilMaterial 中移除,并设置 m_UnmaskMaterial 为 null。
        // 5、通知 StencilStateChanged。(通知所有实现 IMaskable 接口的子物体重新计算遮罩。
        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,因为我们需要 在通知子物体Mask状态改变时,让 IsActive 返回正确的值。
            // 疑问 ??? 未理解,为什么调 base.OnDisable 会影响到 IsActive。
            // 实际测试,某 UIBehaviour 的子类的 OnDisable 中,调用 base.OnDisable 前后,其 activeInHierarchy 和 activeSelf 均为 false。
            base.OnDisable();
            if (graphic != null)
            {
                graphic.SetMaterialDirty();
                graphic.canvasRenderer.hasPopInstruction = false;
                graphic.canvasRenderer.popMaterialCount = 0;    // popMaterialCount:CanvasRenderer组件可用的材质数量,用于内部遮罩。
            }

            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
        // 实现 ICanvasRaycastFilter 的接口
        // 射线投射位置是否有效
        public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
        {
            if (!isActiveAndEnabled)     //若未激活或未启用,则有效(不过滤)
                return true;

            // 若激活且启用,则检查投射点是否在本 rectTransform 的矩形内。 在则有效。
            return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
        }

        // Stencil calculation time!
        // 实际的模板测试在这里进行!
        // 实现 IMaterialModifier 的接口。
        // 1、检查 Mask 是否启用,若未启用,直接返回 baseMaterial。
        public virtual Material GetModifiedMaterial(Material baseMaterial)
        {
            if (!MaskEnabled())
                return baseMaterial;

            var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);   // 获取最深根的 Canvas,或第一个“使用独立绘制顺序”的 Canvas。
            var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas); // 计算模板测试深度。
            if (stencilDepth >= 8)
            {
                Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
                return baseMaterial;    //如果深度>=8,抛出警告,直接返回 baseMaterial。
            }

            int desiredStencilBit = 1 << stencilDepth;  //预期的模板测试深度Bit。

            // if we are at the first level... we want to destroy what is there
            // 如果是嵌套 Mask 的第一层(最上面的一层) 
            if (desiredStencilBit == 1)  // ( 即 stencilDepth == 0)。
            {
                // 创建/获取第一层 Mask 关联的 Gaphic 使用的材质:m_MaskMaterial
                var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
                StencilMaterial.Remove(m_MaskMaterial); 
                m_MaskMaterial = maskMaterial;

                // 创建/获取第一层 Mask 关联的 Gaphic 使用的 pop材质:unmaskMaterial
                var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
                StencilMaterial.Remove(m_UnmaskMaterial); 
                m_UnmaskMaterial = unmaskMaterial;

                graphic.canvasRenderer.popMaterialCount = 1;     // popMaterialCount:CanvasRenderer 组件可用的材质数量,用于内部遮罩。
                graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);  //SetPopMaterial:设置 canvasRenderer 的材质,用于内部遮罩。

                return m_MaskMaterial;  //返回修改后的材质
            }

            //otherwise we need to be a bit smarter and set some read / write masks
            // 否则。需要设置一些 read / write 的遮罩。

            // 创建/获取第N层 Mask 关联的 Gaphic 使用的 pop材质: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;

            // 创建/获取第N层 Mask 关联的 Gaphic 使用的 pop材质: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:CanvasRenderer组件可用的材质数量,用于内部遮罩。
            graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0); //SetPopMaterial:设置 canvasRenderer 的材质,用于内部遮罩。

            return m_MaskMaterial;  //返回修改后的材质
        }
    }
}

3、IMaskable

在 Mask 状态改变时,MaskUtilities 利用这个接口配合对遮罩情况进行更新。

using System;

namespace UnityEngine.UI
{
    // This element is capable of being masked out.
    // 这个元素可以被遮罩。(目前只有MaskableGraphic实现它)
    public interface IMaskable
    {
        // Recalculate masking for this element and all children elements.
        // Use this to update the internal state (recreate materials etc).
        // 重新计算此元素和所有子元素的遮罩。
        // 更新内部状态(重新创建材质等)。
        void RecalculateMasking();
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NRatel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值