一、需求背景与使用场景
在 Unity 的 UI 开发中,遮罩(Mask)是非常常见的功能。
例如:
-
战场小地图的圆形显示区域
-
技能图标的圆形或不规则边框
-
头像或UI动画中柔和渐变的显示窗口
Unity 自带的 Mask 或 RectMask2D 虽然简单易用,但有以下明显问题:
| 问题 | 说明 |
|---|---|
| 锯齿明显 | 默认遮罩为硬边,无柔化过渡 |
| 插件偏重 | SoftMaskForUGUI 插件虽能柔化边缘,但体积大、逻辑复杂 |
| 可控性差 | 无法自定义形状或动态过渡效果 |
✅ 目标:实现一个轻量级的柔和遮罩方案,无需插件,直接在 UI 中使用。
二、解决方案概述
我们的最终方案是:
通过自定义 HLSL Shader + Stencil 模板缓冲机制,实现圆形柔和遮罩(Soft Mask)效果。
✳️ 实现思路
-
父物体:使用自定义 HLSL Shader 写入模板缓冲区(Stencil)。
-
子物体:继续使用普通 UI 材质,Unity 自动执行模板测试。
-
柔化边缘:通过
_Cutoff或遮罩贴图的 Alpha 通道控制平滑度。
💡 方案优点
-
不依赖任何插件
-
无需修改子物体或添加脚本
-
可调节遮罩柔化强度
-
支持圆形或任意形状
-
兼容自定义渲染管线(SRP/URP)
三、实现步骤
1️⃣ 创建遮罩 Shader
创建一个新文件 UI_HLSL_StencilMask.shader,代码如下:
Shader "UI/HLSL_StencilMask"
{
Properties
{
_MaskTex ("Mask Texture", 2D) = "white" {}
_UseMaskTex ("Use Mask Texture", Float) = 1
_Radius ("Circle Radius", Range(0,1)) = 0.45
_Feather ("Edge Feather", Range(0,0.5)) = 0.05
_Cutoff ("Cutoff", Range(0,1)) = 0.01
}
SubShader
{
Tags { "Queue"="Geometry-1" "IgnoreProjector"="True" "RenderType"="Transparent" }
ColorMask 0
ZWrite Off
Cull Off
Blend Off
Pass
{
Name "MaskWrite"
Stencil { Ref 1 Comp Always Pass Replace }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MaskTex;
float _UseMaskTex;
float _Radius, _Feather, _Cutoff;
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };
v2f vert(appdata v)
{
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.uv;
return o;
}
half4 frag(v2f i) : SV_Target
{
float maskA = 1.0;
if (_UseMaskTex >= 0.5)
{
maskA = tex2D(_MaskTex, i.uv).a;
}
else
{
float2 uv = i.uv * 2 - 1;
float dist = length(uv);
maskA = 1.0 - smoothstep(_Radius - _Feather, _Radius + _Feather, dist);
}
if (maskA <= _Cutoff) discard;
return half4(0,0,0,0);
}
ENDHLSL
}
}
}
2️⃣ 创建材质并配置
-
新建材质:命名为
M_UI_SoftMask -
指定 Shader:
UI/HLSL_StencilMask -
在
_MaskTex中导入一张圆形渐变遮罩贴图(带Alpha通道)-
中心白色,边缘透明
-
-
调整参数:
-
_Cutoff:控制边缘硬度 -
_Feather:控制程序遮罩的柔化宽度(仅在无贴图时生效)
-
3️⃣ 在场景中使用
-
创建一个
RawImage,命名为 MaskParent -
将
M_UI_SoftMask材质赋值给它 -
将小地图或其他 UI 元素作为 其子物体
-
完成!无需脚本或子物体材质替换
四、底层原理剖析
🧩 1. 模板缓冲区(Stencil Buffer)
在 GPU 渲染管线中,Stencil 是一个独立的缓冲区,用于控制像素是否可被绘制。
我们的实现流程如下:
-
父物体 Pass
-
根据
_MaskTex或_Radius写入模板值 1 -
仅将“需要显示的部分”写入
-
-
子物体 Pass
-
在绘制时执行模板测试(
Comp Equal Ref 1) -
仅允许模板值为 1 的像素绘制
-
结果:
子 UI 仅在父物体圆形区域内可见,实现了遮罩。
🌈 2. 柔化边缘的原理
柔化效果来自两个机制:
| 控制参数 | 作用 |
|---|---|
| MaskTex Alpha | 控制遮罩边缘渐变强度 |
| _Cutoff | 控制写入模板的阈值 |
| _Feather | 程序生成的边缘过渡宽度 |
当使用程序化圆形遮罩时,smoothstep 根据距离实现平滑插值,
产生自然的软边过渡效果。
💡 3. 子物体无需材质修改的原因
Unity 的 CanvasRenderer 会在 UI 渲染阶段自动插入模板测试参数。
因此:
-
父物体只负责写入;
-
子物体会自动受控于模板缓冲区;
-
这就是为什么无需对每个子 RawImage 改材质。
五、功能特性与扩展性
| 功能项 | 说明 |
|---|---|
| ✅ 支持贴图遮罩 | 使用任何带渐变Alpha的自定义形状贴图 |
| ✅ 支持程序遮罩 | _UseMaskTex=0 时用 _Radius + _Feather 生成圆形遮罩 |
| ✅ 支持动态调整 | _Cutoff 可运行时调整柔化边缘 |
| ✅ 兼容 Overlay 模式 | 完全支持 Screen Space - Overlay |
| ✅ 兼容 SRP/URP | 纯 HLSL 实现 |
| ✅ 零脚本依赖 | 无需管理器或插件组件 |
六、性能与工程优化建议
| 优化点 | 建议 |
|---|---|
| MaskTex 质量 | 使用 512×512 PNG,平滑渐变 |
| 材质复用 | 多个遮罩可共享同一材质实例 |
| 参数修改 | 动态改值时建议用 MaterialPropertyBlock |
| Overdraw 控制 | 控制遮罩区域大小,避免冗余像素计算 |
| 队列管理 | 保证遮罩层渲染顺序在被遮罩元素之前 |
七、总结
| 项目 | 内容 |
|---|---|
| 目标 | 实现轻量柔和遮罩(Soft Mask) |
| 方案 | HLSL + Stencil 缓冲实现 |
| 核心参数 | _MaskTex、_Cutoff、_Radius、_Feather |
| 性能 | 单 Pass,极低性能开销 |
| 扩展性 | 支持动态柔化、任意形状、程序生成 |
这套方案在移动端和主机项目中均表现优秀,
无需插件、结构清晰、调参直观,
是理解 Unity UI 底层渲染机制 的极佳实战案例。
八、常见问题 Q&A
Q1:为什么子物体不需要挂材质?
因为 Unity UI 系统自动执行模板测试,只要父 Mask 先写入即可。
Q2:Cutoff 调高后边缘出现锯齿?
降低
_Cutoff或使用更高精度 Alpha 渐变图。
Q3:程序遮罩的 Radius/Feather 没反应?
仅在
_UseMaskTex=0时启用(无贴图模式)。
Q4:能否用作动态开口遮罩?
可以,通过实时修改
_Radius或_Feather实现呼吸/渐开等动画。
九、结语
通过本方案,我们成功实现了:
-
无插件依赖的柔和 UI 遮罩;
-
支持动态调节、可扩展的柔化边缘;
-
完全兼容自定义管线的高性能实现。
它不仅解决了 Unity 原生 Mask 的锯齿问题,
还为 UI 特效设计提供了更灵活的表达空间。
💬 一句话总结:
“用最轻的方式,做最柔的遮罩。”
AI辅助最后这句总结真不错!
8318

被折叠的 条评论
为什么被折叠?



