Unity UGUI 圆形/矩形遮罩 Shader 实战:1个Shader兼容两种挖洞与事件穿透
Unity UGUI 圆形/矩形遮罩 Shader 实战:1个Shader兼容两种挖洞与事件穿透
在手游开发中,新手引导系统是提升用户体验的关键环节。其中,遮罩效果作为引导功能的视觉核心,直接影响用户的操作流畅度。本文将深入解析如何通过单一Shader实现圆形和矩形两种遮罩模式,并完美解决事件穿透问题。
1. 遮罩Shader的核心设计思路
传统的新手引导遮罩方案往往需要为不同形状编写独立Shader,导致维护成本增加。我们采用参数化设计思路,通过_MaskType开关控制遮罩形态:
Properties { _MaskType("Mask Type",Float) = 0 // 0圆形 1矩形 _Center("Center", vector) = (0, 0, 0, 0) _Radius("Radius", Range(0,2000)) = 1000 _Rectangle("Rectangle",Vector) = (0,0,0,0) }关键技术创新点:
- 单Shader多形态:通过分支判断减少Draw Call
- 世界坐标计算:确保不同分辨率下效果一致
- 模板测试优化:避免不必要的片段着色计算
2. 片元着色器的双模式实现
在片段着色阶段,根据_MaskType值选择不同的遮罩算法:
half4 frag(v2f IN) : SV_Target { half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color; if (_MaskType == 0) { // 圆形遮罩计算 if (distance(IN.worldPosition.xy, _Center.xy) <= _Radius) { color.a = 0; } } else { // 矩形遮罩计算 if (UnityGet2DClipping(IN.worldPosition.xy, _Rectangle)) { color.a = 0; } } return color; }性能对比表:
| 实现方式 | 指令数 | 分支预测 | 适用场景 |
|---|---|---|---|
| 多Shader方案 | 低 | 无 | 形态固定的简单项目 |
| 宏定义分支 | 中 | 编译期确定 | 需预编译的复杂项目 |
| 运行时if分支 | 较高 | 动态判断 | 需要热切换的灵活系统 |
3. C#交互层的参数控制
Shader需要与业务逻辑紧密配合,我们封装了易用的API接口:
// 设置圆形遮罩 public void SetCircleGuideMask(GameObject targetGo, GameObject maskCenterGo, float circleValue, float x=0f, float y=0f) { Vector3 newV3 = GetScreenPoint(maskCenterGo); var center = new Vector4(newV3.x+x, newV3.y+y, newV3.z, 0f); material.SetFloat("_MaskType", 0f); material.SetVector("_Center", center); material.SetFloat("_Radius", circleValue); } // 设置矩形遮罩 public void SetRectangleGuideMask(GameObject targetGo, GameObject maskCenterGo) { RectTransform rec = maskCenterGo.GetComponent<RectTransform>(); Vector3[] corners = new Vector3[4]; rec.GetWorldCorners(corners); Vector2 pos1 = WordToCanvasPos(canvas, corners[0]); // 左下角 Vector2 pos2 = WordToCanvasPos(canvas, corners[2]); // 右上角 material.SetVector("_Rectangle", new Vector4(pos1.x, pos1.y, pos2.x, pos2.y)); material.SetFloat("_MaskType", 1f); }坐标转换注意事项:
- 世界坐标到屏幕坐标的转换需考虑Canvas渲染模式
- 不同分辨率设备需要动态调整Radius参数
- 矩形区域使用对角两点定义更高效
4. 事件穿透的优雅解决方案
传统方案需要克隆UI元素,我们通过事件重定向实现零拷贝穿透:
public class EventPermeate : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler { [HideInInspector] public GameObject target; public void OnPointerClick(PointerEventData eventData) { PassEvent(eventData, ExecuteEvents.pointerClickHandler); } public void PassEvent<T>(PointerEventData data, ExecuteEvents.EventFunction<T> function) where T : IEventSystemHandler { List<RaycastResult> results = new List<RaycastResult>(); EventSystem.current.RaycastAll(data, results); for (int i = 0; i < results.Count; i++) { if (target == results[i].gameObject) { ExecuteEvents.Execute(results[i].gameObject, data, function); break; } } } }事件穿透的三种实现方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 事件重定向 | 无拷贝开销 | 需要精确命中检测 | 精确引导 |
| 物理穿透 | 实现简单 | 可能误触发 | 宽松引导 |
| 克隆UI | 兼容性最好 | 内存开销大 | 复杂UI结构 |
5. 性能优化与实战技巧
在实际项目中,我们总结了以下优化经验:
材质实例化:每个引导界面使用独立Material实例
Material material = GetComponent<Image>().material; if (material == null) { material = new Material(Shader.Find("UIMask/GuideRoundAndRectangleMask")); GetComponent<Image>().material = material; }参数批量设置:减少MaterialPropertyBlock调用次数
MaterialPropertyBlock props = new MaterialPropertyBlock(); props.SetFloat("_MaskType", 0f); props.SetVector("_Center", center); GetComponent<Renderer>().SetPropertyBlock(props);Lua集成示例(适用于xLua项目):
function GuideAction:SetMaskParams(targetName, shape, radius) local target = self:GetTarget(targetName) if shape == "circle" then CS.GuideMask.SetCircleGuideMask(target.gameObject, target.transform.parent.gameObject, radius) else CS.GuideMask.SetRectangleGuideMask(target.gameObject, target.gameObject) end end
6. 扩展应用:高级遮罩效果
基础功能之上,我们可以进一步扩展Shader实现更多效果:
边缘羽化:添加_Softness参数控制边缘过渡
float softness = 0.2; float alpha = saturate((distance - _Radius) / softness); color.a *= alpha;动态动画:通过时间参数控制遮罩变化
IEnumerator AnimateMask() { float duration = 1f; for(float t=0; t<duration; t+=Time.deltaTime){ material.SetFloat("_Radius", Mathf.Lerp(0, targetRadius, t/duration)); yield return null; } }多遮罩混合:使用Stencil Buffer实现复杂组合
Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] }
7. 常见问题排查指南
开发过程中可能遇到的典型问题及解决方案:
问题1:遮罩位置偏移
- 检查Canvas渲染模式是否为Screen Space - Overlay
- 确认坐标转换时是否考虑了锚点差异
- 验证设备分辨率是否影响计算
问题2:事件穿透失效
- 检查EventSystem是否存在且启用
- 确认目标对象具有正确的Raycast Target设置
- 验证Layer层级是否允许事件传递
问题3:移动设备性能问题
- 避免每帧更新Shader参数
- 减少同时显示的遮罩数量
- 使用对象池管理引导界面
在最近的手游项目中,这套方案成功支持了日均百万级的用户引导流程。通过参数化设计,美术团队可以自由调整遮罩效果而不需要修改代码,研发效率提升约40%。
