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<IClipper> m_Clippers = new IndexedSet<IClipper>();
            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<RectMask2D> m_Clippers = new List<RectMask2D>();
            // MaskableGraphic实现了IClippable接口
            private List<IClippable> m_ClipTargets = new List<IClippable>();
            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);
                    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<Transform>();
                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坐标是一样的。
  • 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 "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] 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的材质参数,把剪裁区域传进去。