美文网首页
UGUI笔记——Mask遮罩

UGUI笔记——Mask遮罩

作者: 莫忘初心_倒霉熊 | 来源:发表于2021-06-24 15:59 被阅读0次

    1.0 UGUI Mask遮罩

    UGUI为我们提供了2个遮罩组件,分别是RectMask2D和Mask。下面分别说一下,这俩个遮罩的实现原理与区别。

    2.0 RectMask2D组件

    RectMask2D类结构如下图,实现了IClipper与ICanvasRaycastFilter接口。


    RectMask2D类图
    • 前面我们有介绍过,当UGUI需要重绘UI Mesh时,会调用CanvasUpdateRegistry.PerformUpdate()方法,在该方法中会先更新Layout,然后调用 ClipperRegistry.instance.Cull();进行裁剪,之后更新Render,进行Mesh重绘。

    CanvasUpdateRegistry.cs部分源码如下:

            private void PerformUpdate()
            {
                UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
                CleanInvalidItems();
    
                m_PerformingLayoutUpdate = true;
    
                m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
                //省略代码 更新layout
    
                for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
                    m_LayoutRebuildQueue[i].LayoutComplete();
    
                instance.m_LayoutRebuildQueue.Clear();
                m_PerformingLayoutUpdate = false;
    
                // now layout is complete do culling...
                ClipperRegistry.instance.Cull();
    
                m_PerformingGraphicUpdate = true;
                //省略代码 更新Render
    
                for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
                    m_GraphicRebuildQueue[i].GraphicUpdateComplete();
    
                instance.m_GraphicRebuildQueue.Clear();
                m_PerformingGraphicUpdate = false;
                UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
            }
    
    • ClipperRegistry管理着所有的IClipper对象,在Layout更新之后,通过Cull()方法,遍历IClipper的PerformClipping()方法,执行具体的裁剪逻辑。

    ClipperRegistry.cs部分源码如下:

    public class ClipperRegistry
        {
            readonly IndexedSet<IClipper> m_Clippers = new IndexedSet<IClipper>();
    
            /// <summary>
            /// Perform the clipping on all registered IClipper
            /// </summary>
            public void Cull()
            {
                for (var i = 0; i < m_Clippers.Count; ++i)
                {
                    m_Clippers[i].PerformClipping();
                }
            }
            public static void Register(IClipper c)
            {
                if (c == null)
                    return;
                instance.m_Clippers.AddUnique(c);
            }
            public static void Unregister(IClipper c)
            {
                instance.m_Clippers.Remove(c);
            }
        }
    
    • 我们的RectMask2D组件实现了IClipper接口,并且在OnEnable()注册和OnDisable()注销到ClipperRegistry的m_Clippers集合中。

    RectMask2D.cs部分源码如下:

        public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
        {
            protected override void OnEnable()
            {
                base.OnEnable();
                m_ShouldRecalculateClipRects = true;
                ClipperRegistry.Register(this);
                MaskUtilities.Notify2DMaskStateChanged(this);
            }
            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();
                m_ClipTargets.Clear();
                m_Clippers.Clear();
                ClipperRegistry.Unregister(this);
                MaskUtilities.Notify2DMaskStateChanged(this);
            }
        }
    
    • 同时调用MaskUtilities.Notify2DMaskStateChanged(this);通知该RectMask2D下的所有的IClippable(Text,Image,RawImage)遮罩状态发生改变。
      MaskUtilities提供了遮罩的具体处理逻辑,包括RectMask2D和Mask组件,这里先介绍RectMask2D相关代码,后文再介绍Mask相关代码。

    MaskUtilities.cs部分源码如下:

            public static void Notify2DMaskStateChanged(Component mask)
            {
                var components = ListPool<Component>.Get();
                mask.GetComponentsInChildren(components);
                for (var i = 0; i < components.Count; i++)
                {
                    if (components[i] == null || components[i].gameObject == mask.gameObject)
                        continue;
    
                    var toNotify = components[i] as IClippable;
                    if (toNotify != null)
                        toNotify.RecalculateClipping();
                }
                ListPool<Component>.Release(components);
            }
    
    • Text,Image,RawImage都继承MaskableGraphic,MaskableGraphic实现了IClippable接口。
      MaskUtilities.GetRectMaskForClippable(this)会获取到该MaskableGraphic父级第一个有效的RectMask2D组件。
    • 然后将该MaskableGraphic从之前的RectMask2D中剔除,并通过UpdateCull(false)加入待重建的队列中,最后,将该MaskableGraphic添加到新的RectMask2D组件中。
    • 除此之外MaskableGraphic的OnEnable(),OnDisable(),OnTransformParentChanged()都会调用UpdateClipParent()用于将该MaskableGraphic添加或者移除到对应的RectMask2D中。

    MaskableGraphic.cs部分源码如下:

        public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
        {
            private void UpdateClipParent()
            {
                var newParent = (maskable && IsActive()) ? MaskUtilities.GetRectMaskForClippable(this) : null;
    
                // if the new parent is different OR is now inactive
                if (m_ParentMask != null && (newParent != m_ParentMask || !newParent.IsActive()))
                {
                    m_ParentMask.RemoveClippable(this);
                    UpdateCull(false);
                }
    
                // don't re-add it if the newparent is inactive
                if (newParent != null && newParent.IsActive())
                    newParent.AddClippable(this);
    
                m_ParentMask = newParent;
            }
            /// <summary>
            /// See IClippable.RecalculateClipping
            /// </summary>
            public virtual void RecalculateClipping()
            {
                UpdateClipParent();
            }
            protected override void OnEnable()
            {
                base.OnEnable();
                m_ShouldRecalculateStencil = true;
                UpdateClipParent();
                SetMaterialDirty();
    
                if (GetComponent<Mask>() != null)
                {
                    MaskUtilities.NotifyStencilStateChanged(this);
                }
            }
    
            protected override void OnDisable()
            {
                base.OnDisable();
                m_ShouldRecalculateStencil = true;
                SetMaterialDirty();
                UpdateClipParent();
                StencilMaterial.Remove(m_MaskMaterial);
                m_MaskMaterial = null;
    
                if (GetComponent<Mask>() != null)
                {
                    MaskUtilities.NotifyStencilStateChanged(this);
                }
            }
            protected override void OnTransformParentChanged()
            {
                base.OnTransformParentChanged();
    
                if (!isActiveAndEnabled)
                    return;
    
                m_ShouldRecalculateStencil = true;
                UpdateClipParent();
                SetMaterialDirty();
            }
         }
    
    • 之前我们有讲过CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this)将UI添加到待重建的Graphic队列中。
            private void UpdateCull(bool cull)
            {
                if (canvasRenderer.cull != cull)
                {
                    canvasRenderer.cull = cull;
                    UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
                    m_OnCullStateChanged.Invoke(cull);
                    OnCullingChanged();
                }
            }
            public virtual void OnCullingChanged()
            {
                if (!canvasRenderer.cull && (m_VertsDirty || m_MaterialDirty))
                {
                    /// When we were culled, we potentially skipped calls to <c>Rebuild</c>.
                    CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
                }
            }
    
    • RectMask2D中m_ClipTargets 管理着该遮罩下所有的待裁剪的IClippable对象。
      RectMask2D.cs部分源码如下:
        public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
        {
            [NonSerialized]
            private HashSet<IClippable> m_ClipTargets = new HashSet<IClippable>();
            /// <summary>
            /// Add a IClippable to be tracked by the mask.
            /// </summary>
            /// <param name="clippable">Add the clippable object for this mask</param>
            public void AddClippable(IClippable clippable)
            {
                if (clippable == null)
                    return;
                m_ShouldRecalculateClipRects = true;
                if (!m_ClipTargets.Contains(clippable))
                    m_ClipTargets.Add(clippable);
    
                m_ForceClip = true;
            }
    
            /// <summary>
            /// Remove an IClippable from being tracked by the mask.
            /// </summary>
            /// <param name="clippable">Remove the clippable object from this mask</param>
            public void RemoveClippable(IClippable clippable)
            {
                if (clippable == null)
                    return;
    
                m_ShouldRecalculateClipRects = true;
                clippable.SetClipRect(new Rect(), false);
                m_ClipTargets.Remove(clippable);
    
                m_ForceClip = true;
            }
        }
    
    • 在Remove是会clippable.SetClipRect(new Rect(), false)清除对该clippable的裁剪。canvasRenderer.EnableRectClipping(clipRect)对clipRect以外的区域进行裁剪,canvasRenderer.DisableRectClipping();取消裁剪。

    MaskableGraphic.cs部分源码如下:

            public virtual void SetClipRect(Rect clipRect, bool validRect)
            {
                if (validRect)
                    canvasRenderer.EnableRectClipping(clipRect);
                else
                    canvasRenderer.DisableRectClipping();
            }
    

    以上就是RectMask2D状态改变时,通知该RectMask2D下的所有的IClippable(Text,Image,RawImage)遮罩状态发生改变的所有流程。
    我们再来看看ClipperRegistry,在Layout更新之后,通过Cull()方法,遍历IClipper的PerformClipping()方法,执行的裁剪逻辑。

    • MaskUtilities.GetRectMasksForClip(this, m_Clippers)会获取该节点父级(含自身)所有的RectMask2D组件,保存到m_Clippers(为了处理RectMask2D组件嵌套的情况),相关代码请自行查看源码。
    • Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);会计算出m_Clippers这些RectMask2D的最小可见区域,保存到validRect,相关代码请自行查看源码。
    • 然后遍历m_ClipTargets,将最小可见区域validRect设置给每一个IClippable对象,调用 clipTarget.SetClipRect(clipRect, validRect),上文也介绍了SetClipRect()用于设置裁剪区域。

    RectMask2D.cs部分源码如下:

            public virtual void PerformClipping()
            {
                if (ReferenceEquals(Canvas, null))
                {
                    return;
                }
    
                //TODO See if an IsActive() test would work well here or whether it might cause unexpected side effects (re case 776771)
    
                // if the parents are changed
                // or something similar we
                // do a recalculate here
                if (m_ShouldRecalculateClipRects)
                {
                    MaskUtilities.GetRectMasksForClip(this, m_Clippers);
                    m_ShouldRecalculateClipRects = false;
                }
    
                // get the compound rects from
                // the clippers that are valid
                bool validRect = true;
                Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
    
                // If the mask is in ScreenSpaceOverlay/Camera render mode, its content is only rendered when its rect
                // overlaps that of the root canvas.
                RenderMode renderMode = Canvas.rootCanvas.renderMode;
                bool maskIsCulled =
                    (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) &&
                    !clipRect.Overlaps(rootCanvasRect, true);
    
                bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
                bool forceClip = m_ForceClip;
    
                // Avoid looping multiple times.
                foreach (IClippable clipTarget in m_ClipTargets)
                {
                    if (clipRectChanged || forceClip)
                    {
                        clipTarget.SetClipRect(clipRect, validRect);
                    }
    
                    var maskable = clipTarget as MaskableGraphic;
                    if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
                        continue;
    
                    // Children are only displayed when inside the mask. If the mask is culled, then the children
                    // inside the mask are also culled. In that situation, we pass an invalid rect to allow callees
                    // to avoid some processing.
                    clipTarget.Cull(
                        maskIsCulled ? Rect.zero : clipRect,
                        maskIsCulled ? false : validRect);
                }
    
                m_LastClipRectCanvasSpace = clipRect;
                m_ForceClip = false;
            }
    
    • 并且调用clipTarget.Cull()方法,将UI添加到待重建的Graphic队列中。UpdateCull()方法,上文有说过。
            public virtual void Cull(Rect clipRect, bool validRect)
            {
                var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
                UpdateCull(cull);
            }
    
    • 下文,我们来看看canvasRenderer.EnableRectClipping(clipRect);为什么可以进行裁剪处理,由于CanvasRenderer组件没有开源,我们需要使用Fram Debugger工具进行查看。
            public virtual void SetClipRect(Rect clipRect, bool validRect)
            {
                if (validRect)
                    canvasRenderer.EnableRectClipping(clipRect);
                else
                    canvasRenderer.DisableRectClipping();
            }
    
    • 打开FrameDebug工具,点击Enable,
      没有RectMask2D组件结果如图:


      没有RectMask2D组件
      FrameDebug 没有RectMask2D组件

      添加了RectMask2D组件结果如图:


      添加RectMask2D组件
    FrameDebug 添加RectMask2D组件
    • 对比可以发现UI默认使用UI/Default Shader,使用RectMask2D会多一个DrawCall,没有启用Stencil测试(与Mask组件的区别),开启了一个UNITY_UI_CLIP_RECT的shader变体(可以理解为C#中的宏定义),_ClipRect为可显示区域。
      既然使用了UI/Default Shader,那我们就来看看该shader的源码(请自行到官网下载),看一下UNITY_UI_CLIP_RECT的作用。

    UI/Default Shader部分源码如下:

                fixed4 frag(v2f IN) : SV_Target
                {
                    half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    
                    #ifdef UNITY_UI_CLIP_RECT
                    color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                    #endif
    
                    #ifdef UNITY_UI_ALPHACLIP
                    clip (color.a - 0.001);
                    #endif
    
                    return color;
                }
    

    可以看到启动UNITY_UI_CLIP_RECT,会执行color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);来设置最终渲染的透明度
    UnityGet2DClipping的作用,如果IN.worldPosition.xy在_ClipRect区域,则返回1,否则返回0,这样就到达了_ClipRect区域可见,_ClipRect以外的区域不可见的作用。
    最终,我们可以确定RectMask2D是通过设置透明度的方式,让某些区域不可见的。不过,RectMask2D只能用于裁剪矩形区域。

    3.0 Mask组件

    Mask组件涉及到了shader的模板测试,相关内容,可以看下这篇文章模板测试介绍,这里就不介绍了。

    Mask类结构如下图,实现了IMaterialModifier与ICanvasRaycastFilter接口。

    Mask类图
    • 之前在UI Mesh重建的文章中,讲过当UI的材质,贴图变动时,会添加到CanvasUpdateRegistry中的m_GraphicRebuildQueue队列中,当Unity 执行Canvas.willRenderCanvases事件时,会触发Graphic的UpdateMaterial()方法,进行更新。
    • materialForRendering是个属性方法,会获取该GameObject所有的IMaterialModifier对象,Mask与MaskableGraphic(Text,Image,RawImage)实现了该接口,调用对应的重写方法GetModifiedMaterial()。

    Graphic.cs部分源码如下:

            protected virtual void UpdateMaterial()
            {
                if (!IsActive())
                    return;
    
                canvasRenderer.materialCount = 1;
                canvasRenderer.SetMaterial(materialForRendering, 0);
                canvasRenderer.SetTexture(mainTexture);
            }
            public virtual Material materialForRendering
            {
                get
                {
                    var components = ListPool<Component>.Get();
                    GetComponents(typeof(IMaterialModifier), components);
    
                    var currentMat = material;
                    for (var i = 0; i < components.Count; i++)
                        currentMat = (components[i] as IMaterialModifier).GetModifiedMaterial(currentMat);
                    ListPool<Component>.Release(components);
                    return currentMat;
                }
            }
    
    • MaskUtilities.FindRootSortOverrideCanvas(transform)获取该MaskableGraphic父级的Canvas组件,相关代码请自行查看。
    • MaskUtilities.GetStencilDepth(transform, rootCanvas) 获取该MaskableGraphic相对于rootCanvas的深度值,相关代码请自行查看。

    MaskableGraphic.cs部分源码如下:

            public virtual Material GetModifiedMaterial(Material baseMaterial)
            {
                var toUse = baseMaterial;
    
                if (m_ShouldRecalculateStencil)
                {
                    var rootCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
                    m_StencilValue = maskable ? MaskUtilities.GetStencilDepth(transform, rootCanvas) : 0;
                    m_ShouldRecalculateStencil = false;
                }
    
                // if we have a enabled Mask component then it will
                // generate the mask material. This is an optimisation
                // it adds some coupling between components though :(
                Mask maskComponent = GetComponent<Mask>();
                if (m_StencilValue > 0 && (maskComponent == null || !maskComponent.IsActive()))
                {
                    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.cs部分源码如下:

            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.LogError("Attempting to use a stencil mask with depth > 8", gameObject);
                    return baseMaterial;
                }
    
                int desiredStencilBit = 1 << stencilDepth;
    
                // if we are at the first level...
                // we want to destroy what is there
                if (desiredStencilBit == 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;
                }
    
                //otherwise we need to be a bit smarter and set some read / write masks
                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_UI_ALPHACLIP
            public static Material Add(Material baseMat, int stencilID, StencilOp operation, CompareFunction compareFunction, ColorWriteMask colorWriteMask, int readMask, int writeMask)
            {
               //代码省略...
                var newEnt = new MatEntry();
                newEnt.count = 1;
                newEnt.baseMat = baseMat;
                newEnt.customMat = new Material(baseMat);
                newEnt.customMat.hideFlags = HideFlags.HideAndDontSave;
                newEnt.stencilId = stencilID;
                newEnt.operation = operation;
                newEnt.compareFunction = compareFunction;
                newEnt.readMask = readMask;
                newEnt.writeMask = writeMask;
                newEnt.colorMask = colorWriteMask;
                newEnt.useAlphaClip = operation != StencilOp.Keep && writeMask > 0;
    
                newEnt.customMat.name = string.Format("Stencil Id:{0}, Op:{1}, Comp:{2}, WriteMask:{3}, ReadMask:{4}, ColorMask:{5} AlphaClip:{6} ({7})", stencilID, operation, compareFunction, writeMask, readMask, colorWriteMask, newEnt.useAlphaClip, baseMat.name);
    
                newEnt.customMat.SetInt("_Stencil", stencilID);
                newEnt.customMat.SetInt("_StencilOp", (int)operation);
                newEnt.customMat.SetInt("_StencilComp", (int)compareFunction);
                newEnt.customMat.SetInt("_StencilReadMask", readMask);
                newEnt.customMat.SetInt("_StencilWriteMask", writeMask);
                newEnt.customMat.SetInt("_ColorMask", (int)colorWriteMask);
    
                // left for backwards compatability
                if (newEnt.customMat.HasProperty("_UseAlphaClip"))
                    newEnt.customMat.SetInt("_UseAlphaClip", newEnt.useAlphaClip ? 1 : 0);
    
                if (newEnt.useAlphaClip)
                    newEnt.customMat.EnableKeyword("UNITY_UI_ALPHACLIP");
                else
                    newEnt.customMat.DisableKeyword("UNITY_UI_ALPHACLIP");
    
                m_List.Add(newEnt);
                return newEnt.customMat;
            }
    
    • 依旧是UI/Default Shader文件,开启UNITY_UI_ALPHACLIP之后,会使用clip ()方法进行裁剪
                fixed4 frag(v2f IN) : SV_Target
                {
                    half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
    
                    #ifdef UNITY_UI_CLIP_RECT
                    color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
                    #endif
    
                    #ifdef UNITY_UI_ALPHACLIP
                    clip (color.a - 0.001);
                    #endif
    
                    return color;
                }
    
    • 打开FrameDebug工具,查看结果如下(这里为了说明原理,只考虑有一个Mask的情况,嵌套情况原理是一样的,只是逻辑比较复杂):
    1. 首先是绘制Mask所在的UI对象,开启UNITY_UI_ALPHACLIP,Stencil Ref为1,开启模板测试,Stencil Comp为Always,Stencil Pass为Replace,所以此时Mask所在的摸版检测的缓冲中为1,其它区域为0。


      第一个DrawCall
    2. 然后绘制Mask下需要被裁剪的UI对象,关闭UNITY_UI_ALPHACLIP,Stencil Ref为1,开启模板测试,Stencil Comp为Equal,所以只有与模板缓存相同的值(1)才能通过测试,也就是Mask遮罩所在的区域,因此Mask以外的区域不会显示,Stencil Pass为Keep表示,不会改变模板缓存里的值。

    第二个DrawCall

    3.最后,通过一个DrawCall,开启UNITY_UI_ALPHACLIP,Stencil Comp为Always,表示总是通过检测,Stencil Pass为0,表示将模板缓存里的值赋值为0,以此达到还原模板缓存的目的。


    第三个DrawCall

    可看出来,Mask组件使用了3个DrawCall,而且会破坏被裁剪的UI与其他UI的Mesh合并,因此尽量不要使用Mask组件。非要使用的话,尽量使用RectMask2D代替Mask。

    • 与RectMask2D相同,当Mask组件改变时,也会通知给它下面所有的IMaskable对象(MaskableGraphic实现了该接口)

    Mask.cs部分源码如下:

            protected override void OnEnable()
            {
                base.OnEnable();
                if (graphic != null)
                {
                    graphic.canvasRenderer.hasPopInstruction = true;
                    graphic.SetMaterialDirty();
                }
    
                MaskUtilities.NotifyStencilStateChanged(this);
            }
    
            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();
                if (graphic != null)
                {
                    graphic.SetMaterialDirty();
                    graphic.canvasRenderer.hasPopInstruction = false;
                    graphic.canvasRenderer.popMaterialCount = 0;
                }
    
                StencilMaterial.Remove(m_MaskMaterial);
                m_MaskMaterial = null;
                StencilMaterial.Remove(m_UnmaskMaterial);
                m_UnmaskMaterial = null;
    
                MaskUtilities.NotifyStencilStateChanged(this);
            }
    
    • 遍历所有的IMaskable,进行RecalculateMasking()通知。

    MaskUtilities.cs部分源码如下:

            public static void NotifyStencilStateChanged(Component mask)
            {
                var components = ListPool<Component>.Get();
                mask.GetComponentsInChildren(components);
                for (var i = 0; i < components.Count; i++)
                {
                    if (components[i] == null || components[i].gameObject == mask.gameObject)
                        continue;
    
                    var toNotify = components[i] as IMaskable;
                    if (toNotify != null)
                        toNotify.RecalculateMasking();
                }
                ListPool<Component>.Release(components);
            }
    
    • RecalculateMasking()方法中调用SetMaterialDirty(),将改UI添加到Graphic待重建队列中。

    MaskableGraphic.cs部分源码如下:

            public virtual void RecalculateMasking()
            {
                // Remove the material reference as either the graphic of the mask has been enable/ disabled.
                // This will cause the material to be repopulated from the original if need be. (case 994413)
                StencilMaterial.Remove(m_MaskMaterial);
                m_MaskMaterial = null;
                m_ShouldRecalculateStencil = true;
                SetMaterialDirty();
            }
    

    4.0 ICanvasRaycastFilter接口

    需要注意的是:无论是使用Mask,还是RectMask2D组件,去裁剪一个Button,当我们去点击这个Button被裁剪的区域,其实这个Button是收到点击事件的,但为什么没有触发对应的OnClick事件呢?这个就是Mask和RectMask2D需要实现ICanvasRaycastFilter接口的原因。
    我们知道,所有的Button的事件接受都是依靠Graphic对象的。下面是Graphic检测时的代码,可以看到这里并没有对裁剪区域进行判断,那是怎么过滤掉裁剪区域点击的呢?

    Graphic.cs 部分源码如下:

            public virtual bool Raycast(Vector2 sp, Camera eventCamera)
            {
                if (!isActiveAndEnabled)
                    return false;
    
                var t = transform;
                var components = ListPool<Component>.Get();
    
                bool ignoreParentGroups = false;
                bool continueTraversal = true;
    
                while (t != null)
                {
                    t.GetComponents(components);
                    for (var i = 0; i < components.Count; i++)
                    {
                        var canvas = components[i] as Canvas;
                        if (canvas != null && canvas.overrideSorting)
                            continueTraversal = false;
    
                        var filter = components[i] as ICanvasRaycastFilter;
    
                        if (filter == null)
                            continue;
    
                        var raycastValid = true;
    
                        var group = components[i] as CanvasGroup;
                        if (group != null)
                        {
                            if (ignoreParentGroups == false && group.ignoreParentGroups)
                            {
                                ignoreParentGroups = true;
                                raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                            }
                            else if (!ignoreParentGroups)
                                raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                        }
                        else
                        {
                            raycastValid = filter.IsRaycastLocationValid(sp, eventCamera);
                        }
    
                        if (!raycastValid)
                        {
                            ListPool<Component>.Release(components);
                            return false;
                        }
                    }
                    t = continueTraversal ? t.parent : null;
                }
                ListPool<Component>.Release(components);
                return true;
            }
    

    原因在于一个Button点击之后,还会遍历它的所有父对象,并且获取当前遍历的GameObject的ICanvasRaycastFilter,而Mask和RectMask2D实现了该接口,对应的重写方法如下:

            public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera)
            {
                if (!isActiveAndEnabled)
                    return true;
    
                return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);
            }
    

    会判断当前点击位置是否在改遮罩内部,而我们点击的是被裁剪的区域,自然不在遮罩内部,所以就会返回false,进而在Raycast()方法中过滤掉当前点击。

    相关文章

      网友评论

          本文标题:UGUI笔记——Mask遮罩

          本文链接:https://www.haomeiwen.com/subject/pofbyltx.html