FishPlayer

一个喜欢摸鱼的废物

0%

简单的UI图片挖口

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

因为时间紧迫,用现有的功能糊弄了一个,思路比较蠢: 挖洞区域(父节点,带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
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

[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;
private bool m_hasClipped = false;

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 (!IsInsideRect(selfRectTransform, targetTectTransform))
{
CancelClip();
m_prevClipRect = default;
m_shouldRecalculateClipRects = false;
return;
}

Rect currentRect = selfRectTransform.rect;
Vector2 selfPivot = selfRectTransform.pivot;
// get pivot position
currentRect.position = SwitchToRectTransform(selfRectTransform, targetTectTransform);
// rect's position is bottom left
Vector2 delta = new Vector2(-currentRect.width * selfPivot.x, -currentRect.height * selfPivot.y);
currentRect.position += delta;

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);
m_hasClipped = false;
}

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

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

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

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

#region some utils

private readonly Vector2 RECT_TOP = new Vector2(0.5f, 1f);
private readonly Vector2 RECT_BOTTOM = new Vector2(0.5f, 0f);
private readonly Vector2 RECT_LEFT = new Vector2(0f, 0.5f);
private readonly Vector2 RECT_RIGHT = new Vector2(1f, 0.5f);
private readonly Vector2 RECT_CENTER = new Vector2(0.5f, 0.5f);

private readonly Vector2 RECT_BOTTOM_LEFT = new Vector2(0f, 0f);
private readonly Vector2 RECT_BOTTOM_RIGHT = new Vector2(1f, 0f);
private readonly Vector2 RECT_TOP_LEFT = new Vector2(0f, 1f);
private readonly Vector2 RECT_TOP_RIGHT = new Vector2(1f, 1f);

private Vector2 SwitchToRectTransform(RectTransform from, RectTransform to)
{
Vector2 screenP = RectTransformUtility.WorldToScreenPoint(null, from.position);
RectTransformUtility.ScreenPointToLocalPointInRectangle(to, screenP, null, out Vector2 localPoint);
return localPoint;
}

private bool IsInsideRect(RectTransform rectTransform, RectTransform outterRect)
{
Vector3 checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_BOTTOM_LEFT);
bool bottomLeftOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_BOTTOM_RIGHT);
bool bottomRightOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_TOP_LEFT);
bool topLeftOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);
checkWorldPoint = RectOffsetToWorld(rectTransform, RECT_TOP_RIGHT);
bool topRightOut = !RectTransformUtility.RectangleContainsScreenPoint(outterRect, checkWorldPoint);

return !(bottomLeftOut || bottomRightOut || topLeftOut || topRightOut);
}

private Vector3 RectOffsetToWorld(RectTransform rectTransform, Vector2 normalizedRectPosition)
{
Rect selfRect = rectTransform.rect;
Vector2 localposition = Vector2.zero;
if (RECT_CENTER == normalizedRectPosition) // RectPositionType.Center
{
localposition = selfRect.center;
}
else if (RECT_TOP == normalizedRectPosition) // RectPositionType.Top
{
localposition = new Vector2(selfRect.center.x, selfRect.yMax);
}
else if (RECT_BOTTOM == normalizedRectPosition) // RectPositionType.Bottom
{
localposition = new Vector2(selfRect.center.x, selfRect.yMin);
}
else if (RECT_LEFT == normalizedRectPosition) // RectPositionType.Left
{
localposition = new Vector2(selfRect.xMin, selfRect.center.y);
}
else if (RECT_RIGHT == normalizedRectPosition) // RectPositionType.Right
{
localposition = new Vector2(selfRect.xMax, selfRect.center.y);
}
else if (RECT_TOP_LEFT == normalizedRectPosition) // RectPositionType.TopLeft
{
localposition = new Vector2(selfRect.xMin, selfRect.yMax);
}
else if (RECT_TOP_RIGHT == normalizedRectPosition) // RectPositionType.TopRight
{
localposition = selfRect.max;
}
else if (RECT_BOTTOM_LEFT == normalizedRectPosition) // RectPositionType.BottomLeft
{
localposition = selfRect.min;
}
else if (RECT_BOTTOM_RIGHT == normalizedRectPosition) // RectPositionType.BottomRight
{
localposition = new Vector2(selfRect.xMax, selfRect.yMin);
}
else
{
Debug.LogError($"{normalizedRectPosition} is not a normalized rect position");
}

return rectTransform.TransformPoint(localposition);
}

#endregion

}

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

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