もりもりゲーム制作ブログ

それほどもりもりしてません。Unityの忘備録的ブログ。

【UnityShader】Unityでレーダーっぽい模様のやつをやる(扇形を回転させるShader)

ウィキペディアでいい感じの画像をみつけました。

レーダー - Wikipedia

https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Radar2.gif/220px-Radar2.gif

こういうレーダー?ソナー?がゲーム画面右上にあるだけで絶対かっこよくなると思ったので、近しいことものをやります。やるぞ!



はじめに

今回参考にしたが上の画像くらいで、ゼロからなんとなくで作っていったのでその時々の感情込みで解説していきます。
計算や実装がいつも以上にガバガバだと思います。ご容赦。。。
もっと軽量で明瞭にできるぜ!って人がいたらマジで教えてください。。。わからなすぎて今回の実装にハマっています。。。





完成に必要そうなもの

いきなり作るのは不可能なので実装単位で分割できそうなものをリストアップします。

  • 回転する三角形・・・中心に頂点がくる
  • ・・・・・・・・・三角形にマスクして扇形にする
  • グラデーション・・・扇形にマスクしてソナーっぽくする
  • グリッドガイド・・・べつになくてもいいや(めんどくさがり)

案外簡単そう!!

とりあえず今までの知識でできるものは先に関数化しておく。


円 disc関数

中身のある円をマスクさせるので、対応した単語discと命名しておきます。
*円という単語はリングだったりディスクだったり呼び名が多すぎる

float disc(float2 st) {
	float d = distance(st, float2(0.5, 0.5));
	float s = step(0.5, d);
	return s;
}

今回はめんどくさいのでサイズの変更を引数からはしません。

円の描画についてはこの記事にて
【UnityShader】UnityのShaderでOrbit(惑星軌道) - もりもりゲーム制作ブログ

グリッドガイド

円型グリッドガイドはサイズの違うリングを用意するだけなので、先ほど作ったdisc関数で表現します。
リングの描画もこの記事にて
【UnityShader】UnityのShaderでOrbit(惑星軌道) - もりもりゲーム制作ブログ



回転する三角形

そもそも

そもそも回転する三角形とはなんぞやってことなんですがこんなイメージです

f:id:monimoni114514:20190607014449p:plain
鳥ではないです。

・・・・・・・・・べつに三角形ではないですね。

それはそれとして、
角度θによって切り取られた部分だけ色をつけて、それを回転させられればレーダーっぽいサムシングにはなるんじゃないかと考えました。

考え方

先述のように角度θによって切り取られた部分だけに色を塗ればいいんですね。
???????どうやって???????

  • 任意の座標Pと中心点がなす角度0以上かつ0+θ以下のとき座標Pに色を塗れば上の画像みたいなことができそう・・・・・?
  • 時間に比例してθとかが変化すれば回転するのでは・・・・・?
座標Pと中心点がなすベクトルの角度

とりあえず、任意の座標Pと中心点がなす角度を求める方法すら知らないのでShaderLab(HLSL)における角度についてすこし調べてみました。

👇参考にさせていただいたのはコチラ👇
qiita.com
miso-engine.hatenablog.com

どうやらatan2というものがあるようです。
atanとはアークタンジェント(逆正接)、つまりタンジェント正接)の逆三角関数だそうです。

https://2.bp.blogspot.com/-dGYVe-wn-oc/XAnwFGSSrFI/AAAAAAABQt8/pDKfwLvwCE0GLKggGqjUoQEOWljKVw-CgCLcBGAs/s400/question_head_gakuzen_boy.png
つまりどういうことだ・・・?

タンジェントが角度から底辺と高さの比を表すので
その逆であるアークタンジェントは底辺と高さの比から角度を返します。

atan2(x,y)
引数x,yそれぞれに底辺と高さを入れるとx軸となす角度を返す。

どこを基準としてどのような角度を返すのかがわからなかったのでまずはatan2の値を直接描画させてみます。

fixed4 frag(v2f i) : SV_Target{
	float dx = 0.5 - st.x;//中心点とx座標の差
	float dy = 0.5 - st.y;//中心点とy座標の差
	float rad = atan2(dx, dy);

	return rad;
}

f:id:monimoni114514:20190607024639p:plain
atan2の直接描画

  • 左半分にグラデーション

👉角度が変化している

  • 右半分は真っ黒

👉おそらくマイナスの値
radのかわりにrad+1を返すと右半分にもグラデーションができたので、右側はマイナス値でした。

  • グラデーションが真下から始まる

👉中心から真下に伸びたベクトルが0度を表している

  • 確認できる程度のグラデーション

👉0~1で表された角度が広いので度数法ではない
atan2は度数法ではなく弧度法(ラジアン)で表現する。

弧度法から度数法に変換

見慣れない弧度法のままだと混乱するので、おなじみの度数法に直します。
ラジアンから度に変換するので
ラジアン*180/円周率=度
の式を使います。

#define PI 3.14159265359

fixed4 frag(v2f i) : SV_Target{
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;
	float rad = atan2(dx, dy);
	rad = rad * 180 / PI;//ここで度数法に直す
	rad = rad +180;//ついでに真上を0度(360度)に直す

	return rad;
}

f:id:monimoni114514:20190607144554p:plain
このときのradを描画した

ちょびっとだけ黒くなっている部分が大体0.5度~1度くらいになっている気がするので変換できたみたいです。

角度θで塗り分ける

0度からθ度の間を塗り分けます。
○○以上(以下)のとき色を変えるといえばstep関数です。

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI;
	rad = rad +180;

	float s = step(rad, 60);//radが60以下ならば1を返す

	return s;//1ならば白 0ならば黒を返す
	}

f:id:monimoni114514:20190607151257p:plain
θ=60°

radが60以下のとき白く塗り分けるようになりました。いいぞ!

角度を時間に比例させる

_Timeの値をそのまま返すと、1秒で1度しか変化しないので1000倍くらいしておきます。

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI;
	rad = rad +180;

	float offset = _Time * 1000;//
	float s = step(rad, offset);

	return s;
	}

f:id:monimoni114514:20190608124053g:plain
失敗した・・・

offsetの値が360を超えてしまうと繰り返し回転してくれないみたいです。どうしたものか。

何回でも角度を回転させる

offsetが360を超えたとき0に戻らないのが問題なのですが、_Timeを代入している以上リセットができません。
ということは・・・
n回転するごとに360*n減算すれば0~360の間で値を再現できるのでは・・・?

0回転:_Timeは0°~360° → _Time/360=0.????
1回転:_Time*1000は360°~720° → _Time*1000/360=1.????
2回転:_Time*1000は720°~1080° → _Time*1000/360=2.????
・・・:
n回転:_Time*1000は360*n°~360*(n+1)° → _Time*1000/360=n.????

_Time*1000を360で割った値の整数部から回転数nを取得できますね

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI;
	rad = rad +180;

	float n = floor((_Time * 1000) / 360);//floor関数で整数部を取り出す
	float offset = _Time * 1000 - n * 360;
	float s = step(rad, offset);

	return s;
	}

f:id:monimoni114514:20190608134156g:plain
成功

いいかんじです。

角度θの扇形を回転させる

θ=60°とすると

  • 60°から回転する角度以下
  • 0°から回転する角度以上

この2つの条件どちらにもあてはまるように描画すればできるはずです。

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n = floor((_Time * 1000) / 360);
	float offset = _Time * 1000 - n * 360;
	float s1 = step(rad, 60 + offset);//60°から回転
	float s2 = step(offset, rad);//0°から回転

	return s1*s2;
}

f:id:monimoni114514:20190613004051g:plain
うまくいかない

360付近でおかしなことになりました。

それもそのはず、60°から回転する角度以下という条件は60°進んでいるので、offsetの一周と60°分のラグがあります。
なのでもうひとつ60°分ずらしたnとoffsetを作る必要がありました。

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n1 = floor((_Time * 1000 + 60) / (360));//60°分周回をずらす
	float n2 = floor((_Time * 1000) / 360);
	float offset1 = _Time * 1000 - n1 * 360;//n1ですでにずらしたのでそのまま
	float offset2 = _Time * 1000 - n2 * 360;
	float s1 = step(rad, 60 + offset1);//60°から回転
	float s2 = step(offset2, rad);//0°から回転

	return s1*s2;
}

これでうまくいったかのように思えますがまだです。
そもそも出力も間違えています。

60°から回転する角度以下
0°から回転する角度以上
この二つの条件は下図の赤と青の矢印のように重なったり重ならなかったりしています。
現在はs1*s2を出力しているので2つの矢印が重なったときの図が描画されています。

f:id:monimoni114514:20190613005206p:plain
わかりやすくない図

円の直上角度の基準点(0°と360°)を扇形がまたいでいる間、
赤の角度>θかつθ>青の角度
が成り立たなくなるので、
赤の角度>θまたはθ>青の角度
と書き換える必要があります。

if文が使えればいい感じに

if(扇形が基準点をまたいでいないとき)
	return s1 * s2;
else if(扇形が基準点をまたいでいるとき)
	return s1 + s2;

とできるのですが、
シェーダーにおいてif文はヤバいやつ扱いを受けているそうなのでやめましょう。

かわりにstep関数を使います。
いつもはグラデーションとして使っていますが、

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n1 = floor((_Time * 1000 + 60) / (360));//60°分周回をずらす
	float n2 = floor((_Time * 1000) / 360);
	float offset1 = _Time * 1000 - n1 * 360;//n1ですでにずらしたのでそのまま
	float offset2 = _Time * 1000 - n2 * 360;
	float s1 = step(rad, 60 + offset1);//60°から回転
	float s2 = step(offset2, rad);//0°から回転

	return (s1*s2)*step(offset2, 360 - 60)+
		(s1 + s2)*(1-step(offset2, 360 - 60));
}

f:id:monimoni114514:20190613014934g:plain
できたやつ

おお~~~~~~~~いっすね!
回転する角度できました!

円でマスクする

先ほど作った円を描画するDisc関数でマスクすることで回転する扇形にします!
return文の最後に足すだけ!

fixed4 frag(v2f i) : SV_Target {
	float2 st = i.uv;
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n1 = floor((_Time * 1000 + 60) / (360));//60°分周回をずらす
	float n2 = floor((_Time * 1000) / 360);
	float offset1 = _Time * 1000 - n1 * 360;//n1ですでにずらしたのでそのまま
	float offset2 = _Time * 1000 - n2 * 360;
	float s1 = step(rad, 60 + offset1);//60°から回転
	float s2 = step(offset2, rad);//0°から回転

	return (s1*s2)*step(offset2, 360 - 60)+
		(s1 + s2)*(1-step(offset2, 360 - 60))
		- disc(st);
}

f:id:monimoni114514:20190613020751g:plain
回転する扇形
ね、簡単でしょう?

グラデーション

これも回転するのでgrad_rot関数みたいな名前にしました。

回転する仕組みについては先ほどと同じように作ります。
グラデーションはradとoffsetの値の差を距離関数でそのまま描画します。

と、ここまではよかったのですが、やはり360°をまわったところで問題が発生しました・・・・・


時間の角度と任意の角度の差がおかしくなってしまう(具体的には周回遅れ、周回追い越し)ということなので、時間の角度を3種類用意することにしました。
これにより、周回追い越し、通常周回、周回遅れ、の3パターンの場合の距離関数が取れます。

そしてそれぞれのなかで最も近いものを選別すればいいわけです。

float grad_rot(float2 st) {
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n = floor((_Time * 1000) / 360);
	float offset = _Time * 1000;
	rad = rad + n * 360;

	float d1 = distance(rad, offset) / 50;//現在
	float d2 = distance(rad, offset + 360) / 50;//追い越し
	float d3 = distance(rad, offset - 360) / 50;//遅れ

	return min(min(d1, d2), d3);//min関数:小さいほうの値を返す
}

d1,d2,d3を50で割っていますが、グラデーションが効く幅をここで変えています。引数にするとより良いと思いました。じゃあ直せ

f:id:monimoni114514:20190613032912g:plain
グラデ回転


全部合わせる

必要な物がそろったので全部合わせてみました。

//vert関数
#define PI 3.14159265359
float disc(float2 st, float size) {
	float d = distance(st, float2(0.5, 0.5));
	float s = step(size, d);
	return s;
}

float fan_shape(float2 st, float angle) {
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n1 = floor((_Time * 1000 + angle) / (360));
	float n2 = floor((_Time * 1000) / (360));
	float offset1 = _Time * 1000 - 360 * n1;
	float offset2 = _Time * 1000 - 360 * n2;
	float s1 = step(rad, angle + offset1);
	float s2 = step(offset2, rad);

	return (s1*s2)*step(offset2, 360 - angle) +
	(s1 + s2)*(1 - step(offset2, 360 - angle))
	- disc(st, 0.5);
}

float grad_rot(float2 st) {
	float dx = 0.5 - st.x;
	float dy = 0.5 - st.y;

	float rad = atan2(dx, dy);
	rad = rad * 180 / PI + 180;

	float n = floor((_Time * 1000) / 360);
	float offset = _Time * 1000;
	rad = rad + n * 360;
	float d1 = distance(rad, offset) / 50;
	float d2 = distance(rad, offset + 360) / 50;
	float d3 = distance(rad, offset - 360) / 50;

	return min(min(d1, d2), d3);
}

float grid(float2 st) {
	float r1 = -disc(st, 0.5) + disc(st, 0.495);
	float r2 = -disc(st, 0.3) + disc(st, 0.295);
	float r3 = -disc(st, 0.1) + disc(st, 0.095);
	return r1 + r2 + r3;
}

fixed4 frag(v2f i) : SV_Target
{
	return fan_shape(i.uv, 60) * grad_rot(i.uv) + grid(i.uv);
}
//ENDCG

f:id:monimoni114514:20190613034606g:plain
レーダーとかソナーっぽいやつ


まとめ

今回は基本的、というかほとんど我流で解決したのでかなり感情的な記事になってしまいました。
やたら手順踏んで解決している部分は自分が悩みに悩んだ部分なのでよければ一緒に悩んだ気持ちになってください。

今回作ったシェーダーはこちらにあげてます。
github.com
もし使う日が来たらぜひとも使ってやってください。

緑色に染めたり、縦横のグリッド線に直すとドラゴンレーダーみたいになると思います。

すごく読みにくい説明だったり、ガバガバなアルゴリズムだったりすると思うので、指摘・質問・改善案などありましたらコメントして頂ければ幸いです。