
Mask.cs是 UGUI 中基于模板緩沖區(qū)Stencil Buffer實現(xiàn) UI 遮罩的核心組件。它通過攔截并修改自身及子 UI 元素的渲染材質Material利用 GPU 的 Stencil 測試機制將超出 Mask 矩形區(qū)域外的子 UI 像素“裁剪”掉。同時它還實現(xiàn)了射線過濾防止玩家點到已被遮罩隱藏的區(qū)域。下面是對該源碼核心機制、接口設計、模板緩沖區(qū)Stencil算法及生命周期的全方位深度拆解一、 類頭特性與接口設計[AddComponentMenu(UI/Mask, 13)] [ExecuteAlways] [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier[cite: 2][ExecuteAlways]使得 Mask 在 Editor 編輯模式下不運行游戲也能實時計算和渲染遮罩效果。ICanvasRaycastFilter射線攔截接口實現(xiàn)了IsRaycastLocationValid方法。當鼠標/手指點擊屏幕時UGUI 的射線檢測系統(tǒng)會詢問 Mask“這個點在你的區(qū)域內嗎”如果點擊點落在了 Mask 之外RectangleContainsScreenPoint返回false射線直接被攔截Mask 之外的子節(jié)點將無法接收任何點擊事件。IMaterialModifier材質修改器接口實現(xiàn)了GetModifiedMaterial方法。這是 Mask 最核心的渲染入口用于將常規(guī) UI 材質重寫為帶 Stencil 讀寫指令的動態(tài)材質。二、 關鍵屬性showMaskGraphic[SerializeField] private bool m_ShowMaskGraphic true;[cite: 2]作用控制 Mask 組件所在的Graphic比如自帶的 Image本身是否在屏幕上渲染出來。底層原理在GetModifiedMaterial中如果m_ShowMaskGraphic為false生成的材質會將ColorWriteMask設置為0。效果設置為0后GPU 在渲染該組件時只向 Stencil Buffer 寫入標記而不向 FrameBuffer顏色緩沖區(qū)寫入 RGBA 顏色從而實現(xiàn)“自身不可見但能遮罩子物體”的效果。三、 核心算法GetModifiedMaterial與 Stencil 位運算這是整個Mask.cs最精彩、最硬核的圖形學邏輯。1. 為什么深度限制為 8 層 (stencilDepth 8)var stencilDepth MaskUtilities.GetStencilDepth(transform, rootSortCanvas);[cite: 2] if (stencilDepth 8)[cite: 2] { Debug.LogWarning(Attempting to use a stencil mask with depth 8, gameObject);[cite: 2] return baseMaterial;[cite: 2] }顯卡硬件的Stencil Buffer 通常是 8 位的8-bit取值范圍是 $0 \sim 255$。UGUI 使用按位掩碼Bitmask的方式來實現(xiàn)嵌套遮罩Nested Masking。8 個 Bit 位正好對應最多8 層 Mask 嵌套。如果嵌套超過 8 層Bit 位就會溢出。2. 第一層遮罩desiredStencilBit 1即stencilDepth 0當沒有嵌套只有單層 Mask 時// 1. 生成寫入 Stencil 的材質Mask 自身使用 var maskMaterial StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);[cite: 2] // 2. 生成恢復 Stencil 的 Pop 材質渲染完子物體后使用 var unmaskMaterial StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);[cite: 2] // 3. 將 Pop 材質交給 CanvasRenderer graphic.canvasRenderer.popMaterialCount 1;[cite: 1, 2] graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);[cite: 1, 2]渲染 Mask 自身設置 Stencil 比較函數(shù)為Always總是通過操作為Replace替換寫入?yún)⒖贾?。此時Mask 覆蓋到的屏幕像素在 Stencil Buffer 里值全變成了1。渲染子物體MaskableGraphic子物體的MaskableGraphic也會被MaskUtilities動態(tài)修改材質設置為只有當 Stencil Buffer 的值等于 1時才允許繪制。超出 Mask 范圍的像素Stencil 為 0直接被 GPU 丟棄。清理現(xiàn)場Pop 指令渲染完所有子物體后結合前面提到的CanvasRenderer的 Pop 機制C 引擎會用unmaskMaterial執(zhí)行一次繪制將 Stencil Buffer 中剛才寫入的1抹掉清零StencilOp.Zero防止污染后續(xù)其他同級 UI 元素的渲染。3. 多層嵌套遮罩Nested Masks當出現(xiàn) Mask 里面套 Mask比如滑動列表里面還有一層圓形頭像 Mask時int desiredStencilBit 1 stencilDepth; // 例如第2層 MaskdesiredStencilBit 2 (00000010) var maskMaterial2 StencilMaterial.Add( baseMaterial, desiredStencilBit | (desiredStencilBit - 1), // writeMask StencilOp.Replace, CompareFunction.Equal, m_ShowMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1, // readMask desiredStencilBit | (desiredStencilBit - 1) // bitMask );[cite: 2]嵌套遮罩的邏輯非常巧妙讀取父級通過readMask desiredStencilBit - 1要求當前像素必須已經(jīng)通過了父級 Mask 的 Stencil 測試即父級對應的 Bit 位必須為 1。寫入疊加如果父級測試通過當前 Mask 會將自己的 Bit 位也置為1通過desiredStencilBit | (desiredStencilBit - 1)。Pop 還原渲染完子物體后Pop 材質只清除屬于當前層級的 Bit 位將 Stencil 狀態(tài)還原回父級 Mask 時的狀態(tài)。四、 生命周期與聯(lián)動聯(lián)動生態(tài)protected override void OnEnable() { base.OnEnable(); if (graphic ! null) { graphic.canvasRenderer.hasPopInstruction true;[cite: 1, 2] graphic.SetMaterialDirty();[cite: 2] if (graphic is MaskableGraphic) (graphic as MaskableGraphic).isMaskingGraphic true;[cite: 2] } MaskUtilities.NotifyStencilStateChanged(this);[cite: 2] }Mask 組件并不能獨活它必須與以下幾個底層組件協(xié)同工作graphic.canvasRenderer.hasPopInstruction true 通知底層 C 的CanvasRenderer“我是個遮罩在我把所有子節(jié)點畫完之后請插入一條 Pop 指令用我的popMaterial恢復 Stencil 緩存。”MaskableGraphic Mask 會將自身綁定的 Graphic 的isMaskingGraphic設為true防止該 Graphic 錯誤地將自己當成普通子 UI 搞二次裁剪。MaskUtilities.NotifyStencilStateChanged(this) 這是一個全局通知。當 Mask 被啟用、禁用或在 Editor 中改動時它會向上和向下遞歸通知整條 UI 樹上的所有MaskableGraphic如子節(jié)點的 Text、Image標記材質臟幀SetMaterialDirty促使所有子節(jié)點重新計算并獲取正確的 Stencil 材質。StencilMaterial材質池 源碼中大量調用了StencilMaterial.Add和StencilMaterial.Remove[cite: 2]。UGUI 內部維護了一個靜態(tài)的材質引用計數(shù)池具有相同 Stencil 參數(shù)的材質會被復用[cite: 2]并在OnDisable時通過Remove銷毀并釋放引用[cite: 2]避免產(chǎn)生海量材質實例導致內存泄漏[cite: 2]。總結Mask 組件的工作機制流向[Mask 啟用/修改][cite: 2] │ ▼ [NotifyStencilStateChanged] ── 通知所有子節(jié)點 MaskableGraphic 刷新材質[cite: 2] │ ▼ [GetModifiedMaterial] ── 向 StencilMaterial 申請帶有 Stencil 讀寫指令的 Material[cite: 2] │ ├─? 1. Mask 自身材質: 向 Stencil Buffer 寫入當前層級的 Bit 標記[cite: 2] │ ├─? 2. 設置 popMaterial: 告訴 CanvasRenderer 在末尾清空 Stencil 標記[cite: 1, 2] │ └─? 3. 子物體材質: 檢查 Stencil Buffer 是否有對應 Bit 標記無標記則剪裁丟棄[cite: 2]