深入解析UGUI Mask.cs遮罩核心机制 📅 2026/8/6 4:32:04 Mask.cs是 UGUI 中基于模板缓冲区Stencil Buffer实现 UI 遮罩的核心组件。它通过拦截并修改自身及子 UI 元素的渲染材质Material利用 GPU 的 Stencil 测试机制将超出 Mask 矩形区域外的子 UI 像素“裁剪”掉。同时它还实现了射线过滤防止玩家点到已被遮罩隐藏的区域。下面是对该源码核心机制、接口设计、模板缓冲区Stencil算法及生命周期的全方位深度拆解一、 类头特性与接口设计[AddComponentMenu(UI/Mask, 13)] [ExecuteAlways] [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] public class Mask : UIBehaviour, ICanvasRaycastFilter, IMaterialModifier[cite: 2][ExecuteAlways]使得 Mask 在 Editor 编辑模式下不运行游戏也能实时计算和渲染遮罩效果。ICanvasRaycastFilter射线拦截接口实现了IsRaycastLocationValid方法。当鼠标/手指点击屏幕时UGUI 的射线检测系统会询问 Mask“这个点在你的区域内吗”如果点击点落在了 Mask 之外RectangleContainsScreenPoint返回false射线直接被拦截Mask 之外的子节点将无法接收任何点击事件。IMaterialModifier材质修改器接口实现了GetModifiedMaterial方法。这是 Mask 最核心的渲染入口用于将常规 UI 材质重写为带 Stencil 读写指令的动态材质。二、 关键属性showMaskGraphic[SerializeField] private bool m_ShowMaskGraphic true;[cite: 2]作用控制 Mask 组件所在的Graphic比如自带的 Image本身是否在屏幕上渲染出来。底层原理在GetModifiedMaterial中如果m_ShowMaskGraphic为false生成的材质会将ColorWriteMask设置为0。效果设置为0后GPU 在渲染该组件时只向 Stencil Buffer 写入标记而不向 FrameBuffer颜色缓冲区写入 RGBA 颜色从而实现“自身不可见但能遮罩子物体”的效果。三、 核心算法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的方式来实现嵌套遮罩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 比较函数为Always总是通过操作为Replace替换写入参考值1。此时Mask 覆盖到的屏幕像素在 Stencil Buffer 里值全变成了1。渲染子物体MaskableGraphic子物体的MaskableGraphic也会被MaskUtilities动态修改材质设置为只有当 Stencil Buffer 的值等于 1时才允许绘制。超出 Mask 范围的像素Stencil 为 0直接被 GPU 丢弃。清理现场Pop 指令渲染完所有子物体后结合前面提到的CanvasRenderer的 Pop 机制C 引擎会用unmaskMaterial执行一次绘制将 Stencil Buffer 中刚才写入的1抹掉清零StencilOp.Zero防止污染后续其他同级 UI 元素的渲染。3. 多层嵌套遮罩Nested Masks当出现 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要求当前像素必须已经通过了父级 Mask 的 Stencil 测试即父级对应的 Bit 位必须为 1。写入叠加如果父级测试通过当前 Mask 会将自己的 Bit 位也置为1通过desiredStencilBit | (desiredStencilBit - 1)。Pop 还原渲染完子物体后Pop 材质只清除属于当前层级的 Bit 位将 Stencil 状态还原回父级 Mask 时的状态。四、 生命周期与联动联动生态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 组件并不能独活它必须与以下几个底层组件协同工作graphic.canvasRenderer.hasPopInstruction true 通知底层 C 的CanvasRenderer“我是个遮罩在我把所有子节点画完之后请插入一条 Pop 指令用我的popMaterial恢复 Stencil 缓存。”MaskableGraphic Mask 会将自身绑定的 Graphic 的isMaskingGraphic设为true防止该 Graphic 错误地将自己当成普通子 UI 搞二次裁剪。MaskUtilities.NotifyStencilStateChanged(this) 这是一个全局通知。当 Mask 被启用、禁用或在 Editor 中改动时它会向上和向下递归通知整条 UI 树上的所有MaskableGraphic如子节点的 Text、Image标记材质脏帧SetMaterialDirty促使所有子节点重新计算并获取正确的 Stencil 材质。StencilMaterial材质池 源码中大量调用了StencilMaterial.Add和StencilMaterial.Remove[cite: 2]。UGUI 内部维护了一个静态的材质引用计数池具有相同 Stencil 参数的材质会被复用[cite: 2]并在OnDisable时通过Remove销毁并释放引用[cite: 2]避免产生海量材质实例导致内存泄漏[cite: 2]。总结Mask 组件的工作机制流向[Mask 启用/修改][cite: 2] │ ▼ [NotifyStencilStateChanged] ── 通知所有子节点 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]