UGUI 源码之 RectMask2D、Clipping、RectangularVertexClipper
原圖放在我的 Processon:UGUI RectMask2D+MaskableGraphic 工作原理 | ProcessOn免費在線作圖,在線流程圖,在線思維導圖 |
一、RectMask2D 的實質
1、它的意圖是:在渲染管線的應用階段對其所有子物體進行粗粒度的裁剪和剔除。
2、它先利用?MaskUtilities 類的 GetRectMasksForClip() 方法獲取到與當前 Rectmask2D 共同實際生效的所有父 RectMask2D 列表。
3、再利用?Clipping 類對這些父 RectMask2D 的矩形取交集,得到實際生效的裁剪矩形。
4、然后,它將這個裁剪矩形遍歷地設給了其所有子?MaskableGraphic。
5、最后 MaskableGraphic?調用了?canvasRenderer.EnableRectClipping(clipRect); /?canvasRenderer.DisableRectClipping(); 和 canvasRenderer.cull = cull。(注意,最終還是對CanvasRenderer的操作)。
二、注意
1、要注意,嵌套的?RectMask2D 的遮罩結果是取交集。
具體請看?Clipping 類中的實現。
2、要注意使用獨立繪制順序的 Canvas(overrideSorting==true)對?RectMask2D 遮罩結果的影響。
RectMask2D 的遮罩會被使用獨立繪制順序的 Canvas 打斷。如:下面的情況,RectMask2D將不對 Image 生效。具體請看 MaskUtilities?類的 GetRectMasksForClip() 方法。
//---------------------------------------------------------
// --Canvas1
// ----RectMask2D
// ------Canvas2(使用獨立繪制順序)
// --------Image(clippable)
//---------------------------------------------------------
3、性能優化點
MaskUtilities 類的 GetRectMasksForClip() 方法中,兩層循環都調用了 GetComponentsInParent(),還有?IsDescendantOrSelf() 方法,其性能都和父子物體嵌套的層數有關。
三、全注釋
---------------------- NRatel 割 -------------------------------
更多 UGUI 注釋已放入??GitHub - NRatel/uGUI: Source code for the Unity UI system.。
---------------------- NRatel 割 -------------------------------
1、RectMask2D:
using System; using System.Collections.Generic; using UnityEngine.EventSystems;namespace UnityEngine.UI {[AddComponentMenu("UI/Rect Mask 2D", 13)][ExecuteAlways][DisallowMultipleComponent][RequireComponent(typeof(RectTransform))]// A 2D rectangular mask that allows for clipping / masking of areas outside the mask.// The RectMask2D behaves in a similar way to a standard Mask component. It differs though in some of the restrictions that it has.// A RectMask2D:// *Only works in the 2D plane// *Requires elements on the mask to be coplanar.// *Does not require stencil buffer / extra draw calls// *Requires fewer draw calls// *Culls elements that are outside the mask area.// 一個允許裁剪 Mask 之外區域的2D矩形Mask。// * Only works in the 2D plane. 只在2D平面中工作。// * Requires elements on the mask to be coplanar. 要求元素與Mask在同一平面。// * Does not require stencil buffer / extra draw calls. 不需要模板緩沖區 / 額外的 draw calls。// * Requires fewer draw calls. 需要較少的繪制調用。// * 刪除 Mask 區域之外的元素。public class RectMask2D : UIBehaviour, IClipper, ICanvasRaycastFilter{[NonSerialized]private readonly RectangularVertexClipper m_VertexClipper = new RectangularVertexClipper(); //矩形頂點剔除器[NonSerialized]private RectTransform m_RectTransform; // 與本 RectMask2D 關聯的 RectTransform。[NonSerialized]private HashSet<MaskableGraphic> m_MaskableTargets = new HashSet<MaskableGraphic>(); //所有受本 RectMask2D 裁剪的 MaskableGraphic 的集合。[NonSerialized]private HashSet<IClippable> m_ClipTargets = new HashSet<IClippable>(); //所有受本 RectMask2D 裁剪的 IClippable 的集合 (MaskableGraphic已除外。實際可能為空, 因為 目前實現了 IClippable 接口的只有 MaskableGraphic)。[NonSerialized]private bool m_ShouldRecalculateClipRects; //是否需要重新計算 m_Clippers(臟標記)。[NonSerialized]private List<RectMask2D> m_Clippers = new List<RectMask2D>(); //對于當前節點,自身及實際生效的所有父 RectMask2D 的列表(中間未穿插“使用獨立繪制順序”的Canvas)。[NonSerialized]private Rect m_LastClipRectCanvasSpace; //保存上次生效的、Canvas空間下的裁剪矩形[NonSerialized]private bool m_ForceClip;// Returns a non-destroyed instance or a null reference.// 返回一個未銷毀的 Canvas 或 null。// 當前所屬的 Canvas(激活狀態的)。[NonSerialized] private Canvas m_Canvas;private Canvas Canvas{get{if (m_Canvas == null){var list = ListPool<Canvas>.Get();gameObject.GetComponentsInParent(false, list);if (list.Count > 0)m_Canvas = list[list.Count - 1];elsem_Canvas = null;ListPool<Canvas>.Release(list);}return m_Canvas;}}// Get the Rect for the mask in canvas space.// 獲取 Canvas空間下的 RectMask2D 的 Rect。public Rect canvasRect{get{return m_VertexClipper.GetCanvasRect(rectTransform, Canvas);}}// Helper function to get the RectTransform for the mask.// 獲取 與本 RectMask2D 關聯的 RectTransform。public RectTransform rectTransform{get { return m_RectTransform ?? (m_RectTransform = GetComponent<RectTransform>()); }}protected RectMask2D(){}// 1、調用父類 OnEnable。// 2、m_ShouldRecalculateClipRects 設為 true(需要重新計算 m_Clippers)。// 3、在 ClipperRegistry 注冊。// 4、通知 2DMaskStateChanged(通知所有實現 IClippable 接口的子物體重新計算裁剪。protected override void OnEnable(){base.OnEnable();m_ShouldRecalculateClipRects = true;ClipperRegistry.Register(this);MaskUtilities.Notify2DMaskStateChanged(this);}// 1、調用父類 OnDisable。// 2、清空 m_MaskableTargets。// 3、清空 m_Clippers。// 4、移除 ClipperRegistry 中的注冊。// 5、通知 2DMaskStateChanged。(通知所有實現 IClippable 接口的子物體重新計算裁剪。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();m_ClipTargets.Clear();m_MaskableTargets.Clear();m_Clippers.Clear();ClipperRegistry.Unregister(this);MaskUtilities.Notify2DMaskStateChanged(this); //會調用 GetComponentsInChildren,判斷自身及子物體是否激活。}#if UNITY_EDITOR// 1、調用父類 OnValidate。// 2、m_ShouldRecalculateClipRects 設為 true(需要重新 m_Clippers)。// 4、若當前處于激活狀態,通知 2DMaskStateChanged。protected override void OnValidate(){base.OnValidate();m_ShouldRecalculateClipRects = true;if (!IsActive())return;MaskUtilities.Notify2DMaskStateChanged(this);}#endif// 實現 ICanvasRaycastFilter 的接口// 射線投射位置是否有效public virtual bool IsRaycastLocationValid(Vector2 sp, Camera eventCamera){if (!isActiveAndEnabled) //若未激活或未啟用,則有效(不過濾)return true;// 若激活且啟用,則檢查投射點是否在本 rectTransform 的矩形內。 在則有效。return RectTransformUtility.RectangleContainsScreenPoint(rectTransform, sp, eventCamera);}private Vector3[] m_Corners = new Vector3[4];// rectTransform 在其 Root Canvas 上的矩形private Rect rootCanvasRect{get{// 獲取 rectTransform 四個轉角的世界坐標// 4 個頂點的 返回數組是順時針的。它從左下開始,然后到左上, 然后到右上,最后到右下。// GetWorldCorners: https://docs.unity3d.com/cn/2020.1/ScriptReference/RectTransform.GetWorldCorners.htmlrectTransform.GetWorldCorners(m_Corners);if (!ReferenceEquals(Canvas, null)) //Canvas不為null{Canvas rootCanvas = Canvas.rootCanvas; //取根Canvasfor (int i = 0; i < 4; ++i)m_Corners[i] = rootCanvas.transform.InverseTransformPoint(m_Corners[i]); //將 rectTransform 的四個頂點,變換到 根Canvas 的坐標系下。}// 返回 rectTransform 在其 Root Canvas 上的矩形return new Rect(m_Corners[0].x, m_Corners[0].y, m_Corners[2].x - m_Corners[0].x, m_Corners[2].y - m_Corners[0].y);}}// 執行裁剪// 1、Canvas 為 null 時不執行裁剪。// 2、計算 m_Clippers(僅需要重新計算時)。// 3、判斷是否應該剔除。// 4、為所有子 IClippable 和 所有子 MaskableGraphic 執行裁剪和剔除。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)//TODO 看看 IsActive() 在這里是否能正常工作,或者它是否會導致意外的副作用// if the parents are changed or something similar we do a recalculate here// 如果父物體發生了變化 或 類似的、導致裁剪矩形變化的情況,重新計算 m_Clippers。if (m_ShouldRecalculateClipRects){MaskUtilities.GetRectMasksForClip(this, m_Clippers); //計算 m_Clippers。m_ShouldRecalculateClipRects = false; //置回}// get the compound rects from the clippers that are valid// 用有效的 clippers 獲取疊加/復合的矩形。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.// 如果 Canvas 的渲染模式為 ScreenSpaceOverlay 或 ScreenSpaceCamera,則它的內容只有在它的rect與根Canvas重疊時才會被渲染。// 即: ScreenSpaceOverlay 或 ScreenSpaceCamera 模式下,超過根Canvas的部分會直接被剔除。RenderMode renderMode = Canvas.rootCanvas.renderMode;bool maskIsCulled = (renderMode == RenderMode.ScreenSpaceCamera || renderMode == RenderMode.ScreenSpaceOverlay) && !clipRect.Overlaps(rootCanvasRect, true);//應該被剔除if (maskIsCulled){// 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.// 只有在 Mask 內部才顯示子元素。// 如果 Mask 被剔除,那么 Mask 內的子元素也會被篩選。// 在這種情況下,可以傳遞一個無效的 rect 來讓被調用方避開一些處理。clipRect = Rect.zero;validRect = false;}//裁剪矩形變化了:if (clipRect != m_LastClipRectCanvasSpace){// 為所有子 IClippable 執行裁剪(啟用裁剪并設置裁剪矩形/關閉裁剪)。foreach (IClippable clipTarget in m_ClipTargets){clipTarget.SetClipRect(clipRect, validRect);}// 為所有子 MaskableGraphic 執行裁剪(啟用裁剪并設置裁剪矩形/關閉裁剪)。// 為所有子 MaskableGraphic 執行剔除(設置是否剔除)。foreach (MaskableGraphic maskableTarget in m_MaskableTargets){maskableTarget.SetClipRect(clipRect, validRect);maskableTarget.Cull(clipRect, validRect);}}//強制裁剪:else if (m_ForceClip){// 為所有子 IClippable 執行裁剪(啟用裁剪并設置裁剪矩形/關閉裁剪)。foreach (IClippable clipTarget in m_ClipTargets){clipTarget.SetClipRect(clipRect, validRect);}// 為所有子 MaskableGraphic 執行裁剪(啟用裁剪并設置裁剪矩形/關閉裁剪)。// 為所有子 MaskableGraphic 執行剔除(設置是否剔除)(僅 canvasRenderer.hasMoved 時)。foreach (MaskableGraphic maskableTarget in m_MaskableTargets){maskableTarget.SetClipRect(clipRect, validRect);if (maskableTarget.canvasRenderer.hasMoved) //如果發生的任何更改會使生成的幾何形狀的位置無效,則為 true。maskableTarget.Cull(clipRect, validRect);}}//裁剪矩形未變化且未強制裁剪:else{// 為所有子 MaskableGraphic 執行剔除(設置是否剔除)(僅 canvasRenderer.hasMoved 時)。foreach (MaskableGraphic maskableTarget in m_MaskableTargets){if (maskableTarget.canvasRenderer.hasMoved)maskableTarget.Cull(clipRect, validRect);}}m_LastClipRectCanvasSpace = clipRect; //保存裁剪矩形m_ForceClip = false; //強制裁剪置回}// Add a IClippable to be tracked by the mask.// 添加一個被 RectMask2D 追蹤的 IClippable。public void AddClippable(IClippable clippable){if (clippable == null)return;m_ShouldRecalculateClipRects = true;MaskableGraphic maskable = clippable as MaskableGraphic;if (maskable == null)m_ClipTargets.Add(clippable); //若 IClippable 不是 MaskableGraphic,則加入 m_ClipTargets。elsem_MaskableTargets.Add(maskable); //若 IClippable 是 MaskableGraphic,則加入 m_MaskableTargets。m_ForceClip = true;}// Remove an IClippable from being tracked by the mask.// 移除一個被 RectMask2D 追蹤的 IClippable。public void RemoveClippable(IClippable clippable){if (clippable == null)return;m_ShouldRecalculateClipRects = true;clippable.SetClipRect(new Rect(), false);MaskableGraphic maskable = clippable as MaskableGraphic;if (maskable == null)m_ClipTargets.Remove(clippable); //若 IClippable 不是 MaskableGraphic,則從 m_ClipTargets 中移除。elsem_MaskableTargets.Remove(maskable); //若 IClippable 是 MaskableGraphic,則從 m_MaskableTargets 中移除。m_ForceClip = true;}//重寫 UIBehaviour 的方法//父物體改變后(具體看UIBehaviour里的注釋),// 1、調用父類 OnTransformParentChanged。// 2、m_ShouldRecalculateClipRects 設為 true(需要重新計算 m_Clippers)。protected override void OnTransformParentChanged(){base.OnTransformParentChanged();m_ShouldRecalculateClipRects = true;}//重寫 UIBehaviour 的方法//當關聯的 Canvas 在 Hierarchy 上變化時(具體看UIBehaviour里的注釋),// 1、清除對當前所屬的 Canvas(激活狀態的)的引用。// 2、調用父類 OnCanvasHierarchyChanged。// 3、m_ShouldRecalculateClipRects 設為 true(需要重新計算 m_Clippers)。protected override void OnCanvasHierarchyChanged(){m_Canvas = null;base.OnCanvasHierarchyChanged();m_ShouldRecalculateClipRects = true;}} }2、Clipping
using System.Collections.Generic;namespace UnityEngine.UI {// Utility class to help when clipping using IClipper.// 使用 IClipper 進行裁剪時的工具類。public static class Clipping{// Find the Rect to use for clipping.// Given the input RectMask2ds find a rectangle that is the overlap of all the inputs.// 找到用于剪切的矩形。// 輸入一個 RectMask2d 列表,找到與所有輸入矩形都重疊的矩形。(所有輸入矩形的總交集)// 參數"rectMaskParents":RectMasks to build the overlap rect from. //// 參數"validRect":Was there a valid Rect found.// 返回值:The final compounded overlapping rect.//---------------------------------------------------------------// 這個方法決定了,有多個 RectMask2d 嵌套時,是怎么處理的!//---------------------------------------------------------------public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect){//列表為空,返回無效和默認Rectif (rectMaskParents.Count == 0){validRect = false; return new Rect();}Rect current = rectMaskParents[0].canvasRect; //取第一個 RectMask2D 在 Canvas空間下的 Rect。float xMin = current.xMin;float xMax = current.xMax;float yMin = current.yMin;float yMax = current.yMax;for (var i = 1; i < rectMaskParents.Count; ++i) //遍歷取交集{current = rectMaskParents[i].canvasRect; //取其他 RectMask2D 在 Canvas空間下的 Rect。//取交集:取所有RectMask2D 的 xMin 和 yMin 的最大值 和 xMax 和 yMax 的最小值。if (xMin < current.xMin)xMin = current.xMin;if (yMin < current.yMin)yMin = current.yMin;if (xMax > current.xMax)xMax = current.xMax;if (yMax > current.yMax)yMax = current.yMax;}validRect = xMax > xMin && yMax > yMin; //總交集是否有效if (validRect)return new Rect(xMin, yMin, xMax - xMin, yMax - yMin); //返回總交集elsereturn new Rect();}} }3、RectangularVertexClipper
namespace UnityEngine.UI {internal class RectangularVertexClipper{readonly Vector3[] m_WorldCorners = new Vector3[4];readonly Vector3[] m_CanvasCorners = new Vector3[4];//獲取 t在Canvas下的 rect (包括位置和大小)public Rect GetCanvasRect(RectTransform t, Canvas c){if (c == null)return new Rect();t.GetWorldCorners(m_WorldCorners); //取t的世界坐標var canvasTransform = c.GetComponent<Transform>();for (int i = 0; i < 4; ++i)m_CanvasCorners[i] = canvasTransform.InverseTransformPoint(m_WorldCorners[i]); //將世界坐標轉為相對Canvas的local坐標//返回t在Canvas下的 rect (包括位置和大小)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);}} }總結
以上是生活随笔為你收集整理的UGUI 源码之 RectMask2D、Clipping、RectangularVertexClipper的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: QT自制秒表计时器、可获取电脑时间
- 下一篇: Python网络爬虫简单教程——第一部