回答編集履歴

3

穴をマウスポインタに向かって伸ばす件について追記

2022/04/08 23:35

投稿

Bongo
Bongo

スコア10807

test CHANGED
@@ -22,3 +22,241 @@
22
22
  return lerp(col1, col2, step(dist2, dist1));
23
23
  ```
24
24
 
25
+ ## 穴をマウスポインタに向かって伸ばす件について
26
+
27
+ 下記のような穴のスクリプトと...
28
+
29
+ ```C#
30
+ using System;
31
+ using UnityEngine;
32
+ using UnityEngine.Rendering;
33
+
34
+ [RequireComponent(typeof(Renderer))]
35
+ public class Hole : MonoBehaviour
36
+ {
37
+ private const int SpokeCount = 32;
38
+ private const float SmoothingThreshold = 0.001f;
39
+ private static readonly int CenterProperty = Shader.PropertyToID("_Center");
40
+ private static readonly int InverseMaxSpokeLengthProperty = Shader.PropertyToID("_InverseMaxSpokeLength");
41
+ private static readonly int SpokeLengthsProperty = Shader.PropertyToID("_SpokeLengths");
42
+ private static readonly int SmoothingFactorProperty = Shader.PropertyToID("_SmoothingFactor");
43
+
44
+ [SerializeField][Min(0.0f)] private float maxSpokeLength = 0.4f; // ローカル空間におけるスポークの最大長さ
45
+ [SerializeField][Min(0.0f)] private float mouseAngleHalfWidth = 15.0f; // スポークの長さが半減するマウスポインタとスポークの角度差
46
+
47
+ public Vector2 center; // 穴の中心のローカルXY座標
48
+ [Min(0.0f)] public float smoothTime = 1.0f; // スポークが目標長さに伸縮するおよその時間
49
+ [Range(0.0f, 0.05f)] public float outlineSmoothness = 0.025f; // 穴の輪郭のなめらかさ
50
+ public bool showDebugGizmos; // スポークとマウスポインタ位置をシーンビュー上に表示するか
51
+
52
+ private readonly Vector2[] spokeDirections = new Vector2[SpokeCount];
53
+ private readonly float[] spokeLengths = new float[SpokeCount];
54
+ private readonly float[] spokeTargetLengths = new float[SpokeCount];
55
+ private readonly float[] spokeVelocities = new float[SpokeCount];
56
+ private LocalKeyword enableSmoothing;
57
+ private Camera mainCamera;
58
+ private Material material;
59
+ private float spokeLengthFactor;
60
+
61
+ private void Start()
62
+ {
63
+ this.material = this.GetComponent<Renderer>().material;
64
+ this.enableSmoothing = new LocalKeyword(this.material.shader, "ENABLE_SMOOTHING");
65
+ this.mainCamera = Camera.main;
66
+ for (var i = 0; i < SpokeCount; i++)
67
+ {
68
+ var spokeAngle = (2.0f * Mathf.PI * i) / SpokeCount;
69
+ this.spokeDirections[i] = new Vector2(Mathf.Cos(spokeAngle), Mathf.Sin(spokeAngle));
70
+ }
71
+ this.spokeLengthFactor = (Mathf.Log(0.5f) * Mathf.Rad2Deg * Mathf.Rad2Deg) / (this.mouseAngleHalfWidth * this.mouseAngleHalfWidth);
72
+ }
73
+
74
+ private void Update()
75
+ {
76
+ // まず、マウスポインタの位置をもとに各スポークの目標長さを決める
77
+ // スポークの向きがマウスポインタの方角からずれるほど、スポークの
78
+ // 長さを釣り鐘形に割り引くことでなめらかな外形を作る
79
+ var mouseRay = this.mainCamera.ScreenPointToRay(Input.mousePosition);
80
+ if (!new Plane(this.transform.forward, this.transform.position).Raycast(mouseRay, out var enter))
81
+ {
82
+ return;
83
+ }
84
+ var mouseWorldPosition = mouseRay.GetPoint(enter);
85
+ var mousePosition = (Vector2)this.transform.InverseTransformPoint(mouseWorldPosition);
86
+ var relativeMousePosition = mousePosition - this.center;
87
+ var mouseLength = Mathf.Min(relativeMousePosition.magnitude, this.maxSpokeLength);
88
+ if (mouseLength > 0.0f)
89
+ {
90
+ var mouseDirection = relativeMousePosition.normalized;
91
+ for (var i = 0; i < SpokeCount; i++)
92
+ {
93
+ var angle = Mathf.Acos(Mathf.Clamp(Vector2.Dot(mouseDirection, this.spokeDirections[i]), -1.0f, 1.0f));
94
+ this.spokeTargetLengths[i] = Mathf.Exp(this.spokeLengthFactor * angle * angle) * mouseLength;
95
+ }
96
+ }
97
+ else
98
+ {
99
+ Array.Fill(this.spokeTargetLengths, 0.0f);
100
+ }
101
+
102
+ // 各スポークの長さを目標値に向かってなめらかに伸縮させる
103
+ for (var i = 0; i < SpokeCount; i++)
104
+ {
105
+ this.spokeLengths[i] = Mathf.SmoothDamp(
106
+ this.spokeLengths[i],
107
+ this.spokeTargetLengths[i],
108
+ ref this.spokeVelocities[i],
109
+ this.smoothTime);
110
+ if (this.showDebugGizmos)
111
+ {
112
+ Debug.DrawLine(
113
+ this.transform.TransformPoint(this.center),
114
+ this.transform.TransformPoint((this.spokeDirections[i] * this.spokeLengths[i]) + this.center),
115
+ Color.green);
116
+ }
117
+ }
118
+
119
+ // マウスポインタの位置に十字を表示する
120
+ if (this.showDebugGizmos)
121
+ {
122
+ const float crossRadius = 8.0f;
123
+ var mouseScreenPosition = this.mainCamera.WorldToScreenPoint(mouseWorldPosition);
124
+ Debug.DrawLine(
125
+ this.mainCamera.ScreenToWorldPoint(mouseScreenPosition + (Vector3.left * crossRadius)),
126
+ this.mainCamera.ScreenToWorldPoint(mouseScreenPosition + (Vector3.right * crossRadius)),
127
+ Color.yellow);
128
+ Debug.DrawLine(
129
+ this.mainCamera.ScreenToWorldPoint(mouseScreenPosition + (Vector3.down * crossRadius)),
130
+ this.mainCamera.ScreenToWorldPoint(mouseScreenPosition + (Vector3.up * crossRadius)),
131
+ Color.yellow);
132
+ }
133
+
134
+ // 穴の中心、スポークの長さ、その他マテリアルに必要なデータをセットする
135
+ this.material.SetVector(CenterProperty, this.center);
136
+ this.material.SetFloat(InverseMaxSpokeLengthProperty, 1.0f / this.maxSpokeLength);
137
+ this.material.SetFloatArray(SpokeLengthsProperty, this.spokeLengths);
138
+ this.material.SetKeyword(this.enableSmoothing, this.outlineSmoothness > SmoothingThreshold);
139
+ this.material.SetFloat(SmoothingFactorProperty, 1.0f / this.outlineSmoothness);
140
+ }
141
+
142
+ private void OnDestroy()
143
+ {
144
+ Destroy(this.material);
145
+ }
146
+ }
147
+ ```
148
+
149
+ 板のマテリアル用に下記のようなシェーダーを用意しました。
150
+
151
+ ```ShaderLab
152
+ Shader "Unlit/Hole"
153
+ {
154
+ Properties
155
+ {
156
+ _Color ("Base Color", Color) = (0.0, 0.0, 0.0, 0.5) // 背景の色
157
+ _InnerRadius ("Inner Radius", Range(0.0, 1.0)) = 0.1 // 縁のグラデーションが始まる半径
158
+ _OuterRadius ("Outer Radius", Range(0.0, 1.0)) = 0.2 // 縁のグラデーションが終わる半径
159
+ }
160
+ SubShader
161
+ {
162
+ Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
163
+
164
+ ZWrite Off
165
+ Blend SrcAlpha OneMinusSrcAlpha
166
+
167
+ Pass
168
+ {
169
+ CGPROGRAM
170
+ #pragma multi_compile _ ENABLE_SMOOTHING
171
+ #pragma vertex vert
172
+ #pragma fragment frag
173
+ #include "UnityCG.cginc"
174
+ #define SPOKE_COUNT 32
175
+
176
+ struct v2f
177
+ {
178
+ float2 position : TEXCOORD0;
179
+ float4 vertex : SV_POSITION;
180
+ };
181
+
182
+ v2f vert(float4 vertex : POSITION)
183
+ {
184
+ v2f o;
185
+ o.vertex = UnityObjectToClipPos(vertex);
186
+ o.position = vertex.xy;
187
+ return o;
188
+ }
189
+
190
+ float2 _Center;
191
+ float _InverseMaxSpokeLength;
192
+ float _SpokeLengths[SPOKE_COUNT];
193
+ float _SmoothingFactor;
194
+ fixed4 _Color;
195
+ float _InnerRadius;
196
+ float _OuterRadius;
197
+
198
+ // 線分abと点pの距離を得る
199
+ float distance(float2 a, float2 b, float2 p)
200
+ {
201
+ float2 s = b - a;
202
+ float l2 = dot(s, s);
203
+ if (l2 > 0.0)
204
+ {
205
+ float2 v = p - a;
206
+ float f = dot(v, s) / l2;
207
+ return f <= 0.0 ? distance(a, p) : f >= 1.0 ? distance(b, p) : abs((v.x * s.y) - (v.y * s.x)) / sqrt(l2);
208
+ }
209
+ return distance(a, p);
210
+ }
211
+
212
+ fixed4 frag(v2f i) : SV_Target
213
+ {
214
+ float minimumDistance = 1.#INF;
215
+
216
+ #ifdef ENABLE_SMOOTHING
217
+ float nearness = exp2(-distance(_Center, i.position) * _SmoothingFactor);
218
+ float weightSum = 1.0;
219
+ #endif
220
+
221
+ for (int j = 0; j < SPOKE_COUNT; j++)
222
+ {
223
+ // ピクセルと各スポークの距離を調べて...
224
+ float2 direction;
225
+ sincos(2.0 * UNITY_PI * j / SPOKE_COUNT, direction.y, direction.x);
226
+ float d = distance(_Center, _Center + direction * _SpokeLengths[j], i.position);
227
+
228
+ #ifdef ENABLE_SMOOTHING
229
+ // 外形をスムージングする場合、まずピクセルと各スポークの近さを積算する
230
+ // このとき、スポークの長さに応じて積算量に重みをつける
231
+ float weight = _SpokeLengths[j] * _InverseMaxSpokeLength;
232
+ nearness += exp2(-d * _SmoothingFactor) * weight;
233
+ weightSum += weight;
234
+ #else
235
+ // 外形をスムージングしない場合、単純にピクセルと各スポークの距離を調べて最短のものを選ぶ
236
+ minimumDistance = min(minimumDistance, d);
237
+ #endif
238
+ }
239
+
240
+ // 外形をスムージングする場合、minimumDistanceを積算した近さから逆算する
241
+ #ifdef ENABLE_SMOOTHING
242
+ minimumDistance = -log2(nearness / weightSum) / _SmoothingFactor;
243
+ #endif
244
+
245
+ // 完全透明部分はクリッピングする
246
+ float2 radii = float2(min(_InnerRadius, _OuterRadius), max(_InnerRadius, _OuterRadius));
247
+ clip(minimumDistance - radii.x);
248
+
249
+ // 縁の部分のグラデーションを決める
250
+ fixed4 color = _Color;
251
+ color.a *= smoothstep(radii.x, radii.y, minimumDistance);
252
+ return color;
253
+ }
254
+ ENDCG
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ 穴を中心に仮想的なスポークを放射状に配置し、それらがマウスポインタの位置に応じて伸縮するようにしてみました。シェーダー上ではこれらスポークとの距離をもとに穴を開けるという作戦です。
261
+
262
+ ![図](https://ddjkaamml8q8x.cloudfront.net/questions/2022-04-09/788b4e96-8e7b-4267-8130-b0b26e81c814.gif)

2

関数名の誤字を修正

2022/04/07 21:43

投稿

Bongo
Bongo

スコア10807

test CHANGED
@@ -5,7 +5,7 @@
5
5
  return col2;
6
6
  ```
7
7
 
8
- というのはまずいでしょうね。このように記述しても`flag`から出力される色は`col1`だけで、`col2`は無視されてしまうかと思います。
8
+ というのはまずいでしょうね。このように記述しても`frag`から出力される色は`col1`だけで、`col2`は無視されてしまうかと思います。
9
9
  結局、円Bについては上の方の...
10
10
 
11
11
  ```ShaderLab

1

関数名の誤字を修正

2022/04/07 21:42

投稿

Bongo
Bongo

スコア10807

test CHANGED
@@ -1,4 +1,4 @@
1
- コードの各部分を円A用と円B用に二重化してやるという発想は妥当かと思うのですが、`flag`末尾の...
1
+ コードの各部分を円A用と円B用に二重化してやるという発想は妥当かと思うのですが、`frag`末尾の...
2
2
 
3
3
  ```ShaderLab
4
4
  return col1;