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

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

【UnityShader】Unityで水面の模様を描画するシェーダ・セルラーノイズ

f:id:monimoni114514:20190513192139g:plain
セルラーノイズ
「海底を表現したい!!」

と、少し前に思ってました。
マンタが泳ぐゲームを作りたかったんです。

結局よくわからなくて
・それっぽくファイアアルパカで描いたテクスチャ
・時間に合わせてマテリアルのスケールとオフセットを変化
といった感じにゴリ押しで、以下のようなものができました。

f:id:monimoni114514:20190124155859p:plain
当時限界の海底表現
JoyConの記事:
NintendoSwitchのJoyConのジャイロをUnityで遊ぶ - もりもりゲーム制作ブログ




これはこれでいい感じに海底感があるのでヨシとしてました。


ただ、最近やっとシェーダ勉強しはじめたので、今こそ過去を清算しておこうと思います。

海底とはいってますが、要するに「水面の波が立った部分」を平面に描画するのが目的です。
この模様にぴったりなのがセルラーノイズ!
The Book of Shadersにて解説されています。
thebookofshaders.com

これをShaderLabで記述します。
ほとんどが setchi’s blog様のシェーダーファイルの自己解釈説明になってます。スミマセン。


参考にさせていただいたサイト様

The Book of Shaders

thebookofshaders.com

setchi’s blog

setchi.hatenablog.com
setchi.hatenablog.com

正直この2つを読めば詳しく、正しくわかると思います。


セルラーノイズとは

セルラーノイズです。はい。

さきほどのThe Book of Shadersに詳しく書かれています。

セル。つまり細胞っぽいノイズのことです。

かみくだいて説明すると
まずたくさん並んだマス目があります。

f:id:monimoni114514:20190513184755p:plain
4x4のマス目
このひとつひとつにランダムに動く点をおきます。
f:id:monimoni114514:20190513185225g:plain
マス目をランダムに動く点
これらの点に対して、近ければ黒く、遠ければ白くなるようにすると・・・
2点間(複数点間)に白い線(?)が生まれることになります。
f:id:monimoni114514:20190513192139g:plain
セルラーノイズ

これがセルラーノイズです。


実装

関数の用意

まずはfrag関数内で呼び出す用のcellularnoise()関数を作ります。

float cellularnoise(float2 st, float n){
	
}

st
float2型なのでfloat型が二つ入ります。
i.uvを渡すために使います。
iとはfrag関数が引数にしているものです。
i.uvとすると、メッシュ(今回は平面)の左下を(0,0)右上を(1,1)とするXY平面を取ることができます。

n
ここにいれた値の数だけマスをつくります。
n*nマスのシェーダーを作るために必要です。後述。

マス目に分割

マス目に分割します。
実際にはマス目はないので、さきほどのgifのようなグリッド線は実装しません。

このあと、マスごとに1つだけ点をつくるので、マスが左下から何番目のマスかわかるようにします。

float cellularnoise(float2 st, float n){
	st *= n;
	fst = frac(st);
	ist = floor(st);
	
}

st *= n;
stつまり平面をn倍しています。
fst = frac(st);
floatのstなのでfst。
frac関数を使うと、渡した値の小数部分の値を返してくれます。

frac(0.8)=8
frac(3.4)=4

ist = floor(st);
intのstなのでist。
floor関数を使うと、渡した値の整数部分の値を返してくれます。

floor(0.8)=0
floor(3.4)=3
fstとistなに?
  • fst...分割とfrac

まずi.uvは0~1の値でできたXY平面です。
stにはi.uvが入っていますがst*=n;の行でn倍されています。
つまりstは0~4の値でできたXY平面となりました。

このstの小数点をとるということは、
以下の図の赤文字部分を取ることになります。

f:id:monimoni114514:20190514043820p:plain
マス目とfrac


stはただ4倍しただけなので、
stが表す値の範囲:0.0~4.0
fstには小数点以下を入れているので、
fstが表す値の範囲:0~9
となります。
fstを参照することで座標(1, 1)以上の点でも座標(1, 1)以下のようにふるまうことができるのです。

  • ist...判別とfloor

fstを使ってマス目に分割しましたが、座標(x, y)がどのマスに位置するのか調べるのがfloor関数です。
以下の画像の赤字部分がfloor関数で得られ、どのマスが左から何番目にあるのかまるわかりです。

f:id:monimoni114514:20190514052753p:plain
マス目とfloor
istを参照することでマス目固有の値を取ることもできます。

メイン処理

そもそもこのシェーダーは
すべてのピクセルがランダムに動く点からの最短距離に応じて色を変える
というシェーダーなので、この最短距離を返す変数を宣言しておきます。

float cellularnoise(float2 st, float n){
	st *= n;
	fst = frac(st);
	ist = floor(st);
	//ここまで準備 ここからメイン処理
	float dist = 5;
	
}

float dist = 5;
最短距離を表す変数なので名前はdistanceからdistです。
distにランダムに動く点からの距離を代入し、さらに短い距離があった場合は更新します。
なので、どの点からの距離よりも確実に大きいであろう値の5で初期化します。
2√2より大きければなんでもいいんじゃないかと思います。

最短距離を表す変数distができたので、この中身を更新する処理を作ります。

全マスにあるランダムに動く点からの距離を調べれば最短距離はわかります。
が、全部のマスを調べなくてもできます。

いま処理しているピクセルがあるマスと隣り合ったマスだけを調べれば大体同じことになります。なると思います。たぶん。

f:id:monimoni114514:20190515024250p:plain
白マスより青マスのほうが赤マスに近いはず

	//ここまで準備 ここからメイン処理
	float dist = 5;
	
	for (int y = -1; y <= 1; y++)
		for (int x = -1; x <= 1; x++){
			float2 neighbor = float2(x, y);
		}
}

2重for文
下の画像のように9つのマスを調べるための2重ループ。

f:id:monimoni114514:20190515031810p:plain
2重for文のイメージ

float2 neighbor = float2(x, y);
(x, y)方向にある隣のマスをneihborに入れています。

x=-1
y=1
のとき青いマスが、赤いマスに対してのneighborとなります。

f:id:monimoni114514:20190515040903p:plain
x=-1,y=1のときのneighborの位置


つぎにランダムに動く点を考えます。
マスの中心から上下左右0.5ずつの範囲で動くことにします。

float2 p = 0.5 + 0.5 * sin(_Time.w + 6.2831 * random2(ist + neighbor));

ウオ!これはなに?
混乱してきたので細かく分解してみましょう。

float2 p =
このpという変数が動く点の座標を表します。

0.5 +
0.5というのはマスの左下から0.5足した座標という意味です。
float2型は便利なことに(a, a)の形であればaと記述することができます。

0.5 * sin()
こっちの0.5は範囲の0.5です。
sinは必ず-1~1の値なのでこれを0.5にかけた値は-0.5~0.5に。
これで点はマスの中に留まることになります。

sin(_Time.w +
時間と共に点を移動させるためsin関数で_Timeを使います。

_Time.x → t/20秒
_Time.y → t秒
_Time.z → t*2秒
_Time.w → t*3

6.2831 * random2(ist + neighbor));
6.2831という定数部分はランダム移動の規則性をいじれるみたいです。
0の場合:規則正しく並んだ移動
0.1の場合:タイミングによっては規則正しく並ぶ
といった変化がありました。

random2関数はもともとはないので用意します。

random2関数
float2 random2(float2 st)
{
	st = float2(dot(st, float2(127.1, 311.7)),
		dot(st, float2(269.5, 183.3)));
	return -1.0 + 2.0 * frac(sin(st) * 43758.5453123);
}

まったくわからなかったので丸写しです・・・スミマセン。

random2(ist + neighbor)
これは乱数生成時に、別々のピクセルであっても同じマスであれば一つの点を取るための記述です。

istには現在描画しているピクセルが属すマス(赤マス)の絶対座標が入っています。
neighborは二重for文内で現在調べているマス(青マス)の相対座標を示しています

f:id:monimoni114514:20190515061417p:plain
赤マスとneighbor
ist + neighborという式はneighborマスの絶対座標を表しているので、他マスと乱数が被ることはありません。

ここまでで、隣り合うマスの動く点ができました。
次はこの点と現在の座標の距離を取り、distを最短距離に更新します。

	float2 diff = neighbor + p - fst;
	dist = min(dist, length(diff));

float2 diff = neighbor + p - fst;
diffは差。つまりピクセルと動く点の差ベクトルを取ります。
変数p自体はneighborマス中での座標のため、neighborを足し相対座標にしています。
fstは現在処理しているマスにおけるピクセルの相対座標を表します。

dist = min(dist, length(diff));

min関数

min(a, b)
2つの値を入力するとより小さい方を返します。

length関数

length(float2(x, y))
入力したベクトルの長さを返します。

dist直前に調べた点との距離の内より短い方を代入しdistの最短距離を更新しています。

値を返す

float cellularnoise(float2 st, float n){
	//いままでの処理
	float color = distance * 0.5;
	return color;
}

float color = distance * 0.5;
そのままdistを返するよりこっちのほうがオシャレ。
0.5をかけていますが、この値によって若干明度が変わります。

frag関数内で呼び出す

fixed4 frag(v2f i) : SV_Target{
	return cellularnoise(i.uv, 4);
}


完成!!!!
ではないです。まだ。


色を付けて海面にする。

そもそも海面や海底みたいな模様にしたかったので色を付けます。

水色をかける

とりあえず水色をセルラーノイズにかけてみます。

fixed4 frag(v2f i) : SV_Target{
	return cellularnoise(i.uv, 8) * float4(1, 2, 2, 1);
}

f:id:monimoni114514:20190515070425p:plain
水色をかけたセルラーノイズ
黒すぎますね。

青をたす

黒くなっているということはRGBA値が(0,0,0,1)ということなので単純に青色を足します

fixed4 frag(v2f i) : SV_Target{
	return cellularnoise(i.uv, 8) * float4(1, 2, 2, 1) + float4(0,0,1,1);
}

f:id:monimoni114514:20190515070713p:plain
水色をかけて青を足したセルラーノイズ

これこれ!!!
ということで、完成です。


完成コード

float2 random2(float2 st){
	st = float2(dot(st, float2(127.1, 311.7)),
		dot(st, float2(269.5, 183.3)));
	return -1.0 + 2.0 * frac(sin(st) * 43758.5453123);
}

float cellularnoise(float2 st,float n) {
	st *= n;

	float2 ist = floor(st);
	float2 fst = frac(st);

	float distance = 5;

	for (int y = -1; y <= 1; y++)
		for (int x = -1; x <= 1; x++){
			float2 neighbor = float2(x, y);
			float2 p = 0.5 + 0.5 * sin(_Time.y + 6.2831 * random2(ist + neighbor));

			float2 diff = neighbor + p - fst;
			distance = min(distance, length(diff));
		}

	float color = distance * 0.5;

	return color;
}

fixed4 frag(v2f i) : SV_Target{
	return cellularnoise(i.uv, 4)*0.8*float4(2.05, 1.70, -2, 1)+float4(0.1,0.1,1.5,1);
}




自分のgithubに海っぽいセルラーノイズシェーダあげておきました。
github.com