FishPlayer

一个喜欢摸鱼的废物

0%

UGUI强制引导图片挖口简单版

本周接到了强制引导的功能需求,本周就要做好。我之前没有做过这个需求,说实在的不知道那个图上挖洞的功能怎么做。

因为时间紧迫,用现有的功能糊弄了一个,思路比较蠢: 挖洞区域(父节点,带Mask) -> 被挖洞的目标背景图(子节点,使用网上的mask反转组件)
这个做法有个巨麻烦的点就是当需要聚焦UI空间的时候,需要先让父节点对目标UI,然后让子物体调整尺寸撑满屏幕。预览起来也很麻烦。

最近稍微看了下 UI Mask 的工作原理,想着要不自己做一个方便一些的。于是决定先参考 RectMask2D 的工作原理做一个。

思路

RectMask2D 的工作方法是 给 MaskableGraphic 设置剪裁区域(SetClipRect),区域外的部分都会被裁切不显示。

1
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);

那么我只要稍微反一下,就可以变成区域内的不显示了。

1
color.a *= 1 - UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);

所以最后我们需要一个 UI Shader 赋给被裁切的物体,和一个去调整裁切区域的脚本。

UI Shader (和UI Default基本没区别,就是裁切区域那边反了一下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

Shader "UI/CutTarget"
{
Properties
{
[PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
_Color("Tint", Color) = (1,1,1,1)

_StencilComp("Stencil Comparison", Float) = 8
_Stencil("Stencil ID", Float) = 0
_StencilOp("Stencil Operation", Float) = 0
_StencilWriteMask("Stencil Write Mask", Float) = 255
_StencilReadMask("Stencil Read Mask", Float) = 255

_ColorMask("Color Mask", Float) = 15

[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip("Use Alpha Clip", Float) = 0
}

SubShader
{
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}

Stencil
{
Ref[_Stencil]
Comp[_StencilComp]
Pass[_StencilOp]
ReadMask[_StencilReadMask]
WriteMask[_StencilWriteMask]
}

Cull Off
Lighting Off
ZWrite Off
ZTest[unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask[_ColorMask]

Pass
{
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0

#include "UnityCG.cginc"
#include "UnityUI.cginc"

#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP

struct appdata_t
{
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};

sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;

v2f vert(appdata_t v)
{
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);

OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);

OUT.color = v.color * _Color;
return OUT;
}

fixed4 frag(v2f IN) : SV_Target
{
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;

#ifdef UNITY_UI_CLIP_RECT
// only edit this line :D
color.a *= 1 - UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif

#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif

return color;
}
ENDCG
}
}
}


//#ifndef UNITY_UI_INCLUDED
//#define UNITY_UI_INCLUDED
//
//inline float UnityGet2DClipping(in float2 position, in float4 clipRect)
//{
// float2 inside = step(clipRect.xy, position.xy) * step(position.xy, clipRect.zw);
// return inside.x * inside.y;
//}
//
//inline fixed4 UnityGetUIDiffuseColor(in float2 position, in sampler2D mainTexture, in sampler2D alphaTexture, fixed4 textureSampleAdd)
//{
// return fixed4(tex2D(mainTexture, position).rgb + textureSampleAdd.rgb, tex2D(alphaTexture, position).r + textureSampleAdd.a);
//}
//#endif

调整裁切区域的脚本可以参考 RectMask2D 来写,我只是浅浅试一试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(RectTransform))]
public class CutRectSetter : UIBehaviour, IClipper
{
[SerializeField]
private Canvas _canvas;
[SerializeField]
private MaskableGraphic _clipTarget;

private bool m_shouldRecalculateClipRects = false;
private Rect m_prevClipRect = default;

public void PerformClipping()
{
// IDK why unity use ReferenceEquals, I just copied it from RectMask2D.cs :(
if (ReferenceEquals(_canvas, null) || null == _clipTarget)
{
return;
}

RectTransform selfRectTransform = transform as RectTransform;
RectTransform targetTectTransform = _clipTarget.transform as RectTransform;
if (RectTransformEx.IsNotIntersetedWithTargetRect(selfRectTransform, targetTectTransform))
{
CancelClip();
m_prevClipRect = default;
m_shouldRecalculateClipRects = false;
return;
}

Rect currentRect = selfRectTransform.rect;
Vector2 selfPivot = selfRectTransform.pivot;
// Get pivot position from clip rect
if (RectTransformEx.TryCalculateLocalPositionInAnotherRect(selfRectTransform, targetTectTransform, null, out Vector2 tempLocalPos))
{
currentRect.position = tempLocalPos;
}
else // Cant not calculate, maybe canvas is not valid
{
CancelClip();
m_prevClipRect = default;
m_shouldRecalculateClipRects = false;
return;
}

// Rect's potion is at center point of the screen
Vector2 delta = new Vector2(-currentRect.width * selfPivot.x, -currentRect.height * selfPivot.y);
currentRect.position += delta;

// IDK if it is needed to clamp the clip rect
Rect targetRect = targetTectTransform.rect;
currentRect.xMin = Mathf.Clamp(currentRect.xMin, targetRect.xMin, targetRect.xMax);
currentRect.xMax = Mathf.Clamp(currentRect.xMax, targetRect.xMin, targetRect.xMax);
currentRect.yMin = Mathf.Clamp(currentRect.yMin, targetRect.yMin, targetRect.yMax);
currentRect.yMax = Mathf.Clamp(currentRect.yMax, targetRect.yMin, targetRect.yMax);

m_shouldRecalculateClipRects = m_shouldRecalculateClipRects || m_prevClipRect != currentRect;
if (m_shouldRecalculateClipRects)
{
_clipTarget.SetClipRect(default, true);
_clipTarget.SetClipRect(currentRect, true);
_clipTarget.Cull(currentRect, true);
m_prevClipRect = currentRect;
m_shouldRecalculateClipRects = false;
}
}

[ContextMenu(nameof(CancelClip))]
private void CancelClip()
{
_clipTarget.SetClipRect(default, true);
}

protected override void OnEnable()
{
base.OnEnable();
m_shouldRecalculateClipRects = true;
ClipperRegistry.Register(this);
MaskUtilities.Notify2DMaskStateChanged(this);
}

protected override void OnDisable()
{
CancelClip();
ClipperRegistry.Unregister(this);
MaskUtilities.Notify2DMaskStateChanged(this);
base.OnDisable();
}

protected override void OnTransformParentChanged()
{
m_shouldRecalculateClipRects = true;
}

protected override void OnCanvasHierarchyChanged()
{
m_shouldRecalculateClipRects = true;
}
}

RectTransformEx.cs

试了一下感觉功能可用,调整挖口大小的参考物体无需作为被挖物体的父或子,效果预览非常方便。

但这个做法有两个比较可惜的点
1 此做法只能挖放行的缺口
2 当前只挖缺口,没有做柔软处理,估计还得照着 RectMask2D 的思路去做软化

射线放行

除了给暗色遮罩挖一个口,还需要一个简单小功能,把UI点击的射线放行,同时并检测目标收到点击相应。
在原本用于阻挡射线的整块板子上用ICanvasRaycastFilter来做自定义开口以放行射线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class UIRaycastCutRectSetter : MonoBehaviour, ICanvasRaycastFilter
{
[SerializeField]
private RectTransform[] _cutRects;

public bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
if (_cutRects == null || _cutRects.Length == 0)
{
return false;
}

for (int i = 0, length = _cutRects.Length; i < length; i++)
{
if (RectTransformUtility.RectangleContainsScreenPoint(_cutRects[i], screenPoint, eventCamera))
{
return false; // Pass it
}
}

return true;
}
}