当前位置: 首页 > news >正文

深入解析UGUI Mask.cs遮罩核心机制

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]
  1. [ExecuteAlways]:使得 Mask 在 Editor 编辑模式下不运行游戏也能实时计算和渲染遮罩效果。

  2. ICanvasRaycastFilter(射线拦截接口)

    • 实现了IsRaycastLocationValid方法。当鼠标/手指点击屏幕时,UGUI 的射线检测系统会询问 Mask:“这个点在你的区域内吗?”

    • 如果点击点落在了 Mask 之外,RectangleContainsScreenPoint返回false,射线直接被拦截,Mask 之外的子节点将无法接收任何点击事件

  3. IMaterialModifier(材质修改器接口)

    • 实现了GetModifiedMaterial方法。这是 Mask 最核心的渲染入口,用于将常规 UI 材质重写为带 Stencil 读写指令的动态材质。

二、 关键属性:showMaskGraphic

[SerializeField] private bool m_ShowMaskGraphic = true;[cite: 2]
  • 作用:控制 Mask 组件所在的Graphic(比如自带的 Image)本身是否在屏幕上渲染出来。

  • 底层原理:在GetModifiedMaterial中,如果m_ShowMaskGraphicfalse,生成的材质会将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层 Mask,desiredStencilBit = 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 组件并不能独活,它必须与以下几个底层组件协同工作:

  1. graphic.canvasRenderer.hasPopInstruction = true: 通知底层 C++ 的CanvasRenderer:“我是个遮罩,在我把所有子节点画完之后,请插入一条 Pop 指令,用我的popMaterial恢复 Stencil 缓存。”

  2. MaskableGraphic: Mask 会将自身绑定的 Graphic 的isMaskingGraphic设为true,防止该 Graphic 错误地将自己当成普通子 UI 搞二次裁剪。

  3. MaskUtilities.NotifyStencilStateChanged(this): 这是一个全局通知。当 Mask 被启用、禁用或在 Editor 中改动时,它会向上和向下递归通知整条 UI 树上的所有MaskableGraphic(如子节点的 Text、Image)标记材质脏帧(SetMaterialDirty,促使所有子节点重新计算并获取正确的 Stencil 材质。

  4. StencilMaterial(材质池): 源码中大量调用了StencilMaterial.AddStencilMaterial.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]
http://www.jsqmd.com/news/1337523/

相关文章:

  • 团队高效协作:从设计到落地的实施工作流程实战指南
  • UE5编辑器效率提升:10个实用快捷键与隐藏功能深度解析
  • 2026 年 7 月新发布:东胜靠谱的顶管前后灌浆加固公司哪个好,地下管线不沉降?这玩意儿帮工地省了百万返工费,早知道就用它! - 企业推荐管【认证】
  • 从GameMaker到Godot:核心概念映射与迁移实战指南
  • FPGA高速收发器GT核心原理与实战调试指南
  • 网络诊断利器netstat:从核心参数到实战排查的完整指南
  • 2026 年新消息:梁园可靠的丙烯酸防腐涂料供应厂家哪家好,金属件锈穿的致命隐患,竟被这玩意儿悄悄解决了? - 领域鉴赏官
  • 2026小红书无水印下载,手机电脑免费去水印方法 - 办公小帮手
  • 2026 年更新:双滦靠谱的移动发电机车租赁厂家电话,工地停电抢修急?借它就能2小时补能,这便宜租赁谁赚谁知道?-速达发电机租赁 - 企业推荐管【认证】
  • 软考高级必备:数据流图核心四要素与分层绘制实战解析
  • Nginx连接数监控:从基础原理到实战排查的完整指南
  • Python while循环与pass语句:从基础语法到实战应用全解析
  • LandCover数据全攻略:从选型下载到GEE实战与精度评估
  • 报名照片尺寸怎么弄?从手机到电脑,这5种方法一学就会 - 免费软件工具方法教程
  • 四川成都淘汰变压器回收公司电话怎么选?2026年本地回收服务口碑参考 - 优质品牌商家
  • 国内外编程工具汇总
  • 我花了三天才搞定的“智能狗”编程题,AI也翻车了
  • 国内AI模型大比拼:谁实力最强?
  • C语言小结---2048游戏实现
  • 撤三风险从来不是一纸通知:权大师如何帮助企业建立“一标一档”证据管理机制
  • Java设计模式实战:从单例到策略,代码实现与避坑指南
  • FreeMocap开源动捕指南:用普通摄像头实现低成本3D动作捕捉
  • Unity序列化核心机制:Serializable、SerializeField与SerializeReference深度解析
  • 乌鲁木齐市达坂城区外墙漏水维修_2026新疆中部风谷地区漏水维修流程教程与价格表 - 雨婺虹修缮
  • 2026年8月广东整厂打包回收/广东厂房拆除回收厂家深度推荐_深圳市钰东再生资源回收有限公司 - 品牌宣传支持者
  • 算法面试——哈希表:两数之和、三数之和、最长连续序列
  • 采购河北外墙钢板网厂家如何把控质量与交货周期 - 品牌优推
  • MATLAB偏微分方程求解进阶:从一维非线性到二维有限元实战
  • 终极指南:如何在ESP32上打造情感丰富的AI聊天机器人表情系统
  • 鸣潮工具箱WaveTools完整指南:三步解锁120帧画质优化终极方案