RectMask2D源码简析

0. 版本

  • 源码版本:2017.3.0
  • 着色器版本:2017.3.0

1. PerformClipping调用顺序

  • CanvasUpdateRegistry.PerformUpdate()
    public class CanvasUpdateRegistry
    {
        protected CanvasUpdateRegistry()
        {
            // 每一帧都会调用
            Canvas.willRenderCanvases += PerformUpdate;
        }
        
        private void PerformUpdate()
        {
             // 更新Layout
             ......

             // now layout is complete do culling...
             ClipperRegistry.instance.Cull();

             // 更新Graphic
             ......
        }
    }
  • ClipperRegistry.Cull()
    public class ClipperRegistry
    {
        // RectMask2D实现了IClipper接口
        readonly IndexedSet m_Clippers = new IndexedSet();
        
        public void Cull()
        {
            for (var i = 0; i < m_Clippers.Count; ++i)
            {
                m_Clippers[i].PerformClipping();
            }
        }
    }
  • RectMask2D.PerformClipping()
    public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter
    {
        private List m_Clippers = new List();

        // MaskableGraphic实现了IClippable接口
        private List m_ClipTargets = new List();
        
        public virtual void PerformClipping()
        {
            //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)
            {
                // m_Clippers = this的Parent路径上的,所有的RectMask2D组件。
                MaskUtilities.GetRectMasksForClip(this, m_Clippers);
                m_ShouldRecalculateClipRects = false;
            }

            // get the compound rects from
            // the clippers that are valid
            bool validRect = true;
            // clipRect = m_Clippers中所有的RectMask2D的区域的交集。
            // vaildRect = 区域是否有交集。
            Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);
            bool clipRectChanged = clipRect != m_LastClipRectCanvasSpace;
            if (clipRectChanged || m_ForceClip)
            {
                // 重点1:向MaskGraphic设置剪裁区域。
                // m_ClipTargets有哪些后面会分析。
                foreach (IClippable clipTarget in m_ClipTargets)
                    clipTarget.SetClipRect(clipRect, validRect);

                m_LastClipRectCanvasSpace = clipRect;
                m_LastValidClipRect = validRect;
            }

            foreach (IClippable clipTarget in m_ClipTargets)
            {
                // hasMoved : True if any change has occured that would invalidate the positions of generated geometry. 
                var maskable = clipTarget as MaskableGraphic;
                if (maskable != null && !maskable.canvasRenderer.hasMoved && !clipRectChanged)
                    continue;

                // 重点2:MaskGraphic进行剪裁。  
                clipTarget.Cull(m_LastClipRectCanvasSpace, m_LastValidClipRect);
            }
        }
  • MaskGraphic.SetClipRect
  • MaskGraphic.Cull
    public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
    {       
        public virtual void SetClipRect(Rect clipRect, bool validRect)
        {
            if (validRect)  // Mask有交接,设置区域。修改材质的事被CanvasRenderer做了,黑盒看不到。
                canvasRenderer.EnableRectClipping(clipRect);
            else
                canvasRenderer.DisableRectClipping(); // Mask没有交集。
        }
        
        public virtual void Cull(Rect clipRect, bool validRect)
        {
            var cull = !validRect || !clipRect.Overlaps(rootCanvasRect, true);
            UpdateCull(cull);
        }

        private void UpdateCull(bool cull)
        {
            var cullingChanged = canvasRenderer.cull != cull;
            canvasRenderer.cull = cull;

            if (cullingChanged)
            {
                UISystemProfilerApi.AddMarker("MaskableGraphic.cullingChanged", this);
                m_OnCullStateChanged.Invoke(cull);
                SetVerticesDirty(); // 疑问:为什么cull变了,要重建网格?猜测是CanvasRender根据cull的情况,做了网格的优化,减少了一些Overdraw。可以实验看看。
            }
        }
    }

2. 剪裁区域

  • GetCanvasRect
    internal class RectangularVertexClipper
    {
        readonly Vector3[] m_WorldCorners = new Vector3[4];
        readonly Vector3[] m_CanvasCorners = new Vector3[4];

        public Rect GetCanvasRect(RectTransform t, Canvas c)
        {
            if (c == null)
                return new Rect();
            
            t.GetWorldCorners(m_WorldCorners);
            var canvasTransform = c.GetComponent();
            for (int i = 0; i < 4; ++i)
                m_CanvasCorners[i] = canvasTransform.InverseTransformPoint(m_WorldCorners[i]);

            return new Rect(m_CanvasCorners[0].x, m_CanvasCorners[0].y, m_CanvasCorners[2].x - m_CanvasCorners[0].x, m_CanvasCorners[2].y - m_CanvasCorners[0].y);
        }
    }
  • UI/Default
    疑问:OUT.worldPosition = v.vertex; 读取的是Local坐标,怎么是World坐标呢?
    猜测:CanvasBuildBatch生成的Mesh,这个Mesh直接放到世界坐标系下了,Local坐标是World坐标是一样的。
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "UI/Default"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc" // 2D Mask 剪裁。

            #pragma multi_compile __ UNITY_UI_CLIP_RECT
            #pragma multi_compile __ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1; // 2D Mask 剪裁。
                UNITY_VERTEX_OUTPUT_STEREO
            };

            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect; // 2D Mask 剪裁。

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.worldPosition = v.vertex;  // 2D Mask 剪裁。
                OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

                OUT.texcoord = v.texcoord;

                OUT.color = v.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            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); // 2D Mask 剪裁。
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                return color;
            }
        ENDCG
        }
    }
}

3. m_ClipTargets

// MaskGraphic.cs
        protected override void OnEnable()
        {
            ......
            UpdateClipParent();
            ......
        }

        protected override void OnDisable()
        {
            ......
            UpdateClipParent();
            ......
        }

        protected override void OnTransformParentChanged()
        {
            ......
            UpdateClipParent();
            ......
        }
        protected override void OnCanvasHierarchyChanged()
        {
            ......
            UpdateClipParent();
            ......
        }
        public virtual void RecalculateClipping()
        {
            UpdateClipParent();
        }
// MaskGraphic.cs

        private void UpdateClipParent()
        {
            // 返回最近的ParentMask
            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;
        }
// RectMask2D.cs

        public void AddClippable(IClippable clippable)
        {
            if (clippable == null)
                return;
            m_ShouldRecalculateClipRects = true;
            if (!m_ClipTargets.Contains(clippable))
                m_ClipTargets.Add(clippable);

            m_ForceClip = true;
        }

4. 总结

  • RectMask2D在每帧都会检查剪裁区域是否变化了。PerformUpdate中执行。
  • 剪裁区域 = 当前RectMask2D到Parent路径上的所有有效的RectMask2D的剪裁区域的交集。
  • 如果剪裁区域变化了,会通过CanvasRenderer来间接修改MaskGraphic的材质参数,把剪裁区域传进去。
  • 如果剪裁区域变化了,还会为MaskGraphic调用SetVerticesDirty,重新生成网格。猜测是CanvasRenderer对剪裁的网格进行了优化,避免了OverDraw。代价就是剪裁区域变化的时候,要重新生成网格。
  • 每个MaskGraphic,会把自己注册到,最近的一个ParentMask上面。
  • OUT.worldPosition = v.vertex; 读取的是Local坐标。猜测CanvasBuildBatch生成的Mesh,这个Mesh直接放到世界坐标系下了,Local坐标是World坐标是一样的。
  • 放到CanvasRenderer里面的是Local坐标,怀疑CanvasRenderer,把这个Local坐标又转成World坐标,再放到_ClipRect里的。根据一些测试,直接跳过CanvasRenderer,向_ClipRect直接传入World坐标是对的,传入Local坐标是不对的。

你可能感兴趣的:(RectMask2D源码简析)