・Unityで水面っぽいシェーダーを作ってみよう!【芸工生 Advent Calendar 2020 3日目】
こんにちは!眠野眠太郎と申します。
この記事ではUnityでアセットとかに一切頼らずに作った水面っぽいサーフェイスシェーダーの仕組みを喋ります。シェーダーってこんなんだよ~みたいな解説も挟んでいますが、基本的にはシェーダーの基礎知識がある人向けです。
・芸工生 Advent Calendar 2020の宣伝
Advent Calendarというのはいろんな人で12/1~12/25までに1日1個記事を書こうぜ!!という催しです。Qiitaとか見てる人は聞いたことがあるんじゃないでしょうか。
芸工生 Advent Calendar 2020も同様に、九州大学芸術工学部の人が打ち立てたカレンダーらしいです!九大芸工の人なら誰でも登録していいみたいなので、何かしらの記事を書いてカレンダーページにURLを登録してみると楽しいかもしれません!!!自分は作ったプログラムについて喋りますが、知り合いの人のtipsとか創作論とか思想強めの話とか聞いてみたいです、参戦待ってます!!!!!!!!
芸工生 Advent Calendar 2020のページ → https://adventar.org/calendars/5827
・前置き
今年の芸工祭は初のオンライン開催となりましたが、みなさんは楽しめましたか?これは特設サイト → https://www.geikosai2020.jp/
学祭企画はオンラインであることをしっかりと利用するようなコンテンツが練られていて、他企画ながらすごい!!!と膝を連打しまくりでした。他の展示は当日バタバタしていて見れませんでした、泣いちゃった……。
僕は3研噴水企画とzenyaに所属していたんですが、今年の3研噴水企画では「ワクワク噴水ランド」というバーチャル空間をUnityで制作し、clusterというバーチャルSNSで公開してみんなで遊ぼう!みたいなことをしていました!!
ランドの様子を生中継する放送も行われていたので、ぜひ見てみてください!!!(YouTubeの謎仕様により強制的に年齢制限がかけられていますが……許せねぇ!!!!!!!!!!)
僕はワクワク噴水ランドでオブジェクトのモデリングとか設営とかをやっていたんですが、↓の水面のシェーダーを書いたりもしていました!!!今回はこれについて軽く喋っていきます。
・シェーダーって何!?!?!?
3DCGにはオブジェクトの質感を決めるマテリアルという概念があります。マテリアルで物体の基本色とか金属感とか粗さ・滑らかさ等のパラメーターを調整して3Dオブジェクトの質感を調整するのですが、シェーダーは「マテリアルの中でどういう計算を行うのかを決めるプログラム」みたいなものだと自分は解釈しています。例えば「滑らかさ(smoothness)が1に近いほど光沢を弱くする」とか……いやこの例え適切かわからん!!!!!!!
画像は3DCGソフトblenderのマテリアルの設定画面です。Base ColorとかRoughnessとかありますが、そのうちSurfaceってパラメータがPrincipled BSDFってなってると思います。これは「Principled BSDFっていうシェーダー(プログラム)を使って質感とか見え方を計算するわよ~」ということを表しています。
Unityではこのシェーダーのプログラムのうちすごく簡単な部分を、作る人が自由に書けたりします。次の章からは具体的なコードの話をしていきます!
・ソースコード
早速ソースコードを貼っていきます。使う時はライセンス表示とかは別に要らないんですが、この記事を参考にしたよ~と言ってくれると喜びます!!!
大まかな流れとしては、
①水面の後ろにある画像を取得
↓↓↓↓↓
②法線を歪める
↓↓↓↓↓
③光沢を付ける
って感じです。
Shader "Custom/suimenn"
{
Properties
{
_Scale("Scale", float) = 1.5
_TimeScale("時間のスピード", Vector) = (0, 1, 0, 0)
_Distortion("歪み具合", float) = 1.0
_Spec("反射光", Color) = (0.5, 0.5, 0.5)
_Smooth("滑らかさ", Range(0, 1)) = 1
_Alpha("透明度", Range(0, 1)) = 0.5
[MaterialToggle] _IsTex("元のテクスチャを表示", Float) = 0
_Complex("複雑度", int) = 5
_Effectivity("影響度", float) = 0.5
}
SubShader
{
Tags { "RenderType" = "Transparent"
"Queue" = "Transparent"}
LOD 200
Cull Off
GrabPass{}
CGPROGRAM
#pragma surface surf StandardSpecular alpha:fade
#pragma target 3.0
float _Scale;
fixed4 _TimeScale;
float _Distortion;
fixed4 _Spec;
half _Smooth;
float _Alpha;
int _Complex;
float _Effectivity;
float _IsTex;
sampler2D _GrabTexture;
struct Input
{
float4 screenPos;
float2 uv_MainTex;
float3 worldPos;
};
fixed2 random2(fixed2 st) {
st = fixed2(dot(st, fixed2(127.1, 311.7)),
dot(st, fixed2(269.5, 183.3)));
return -1.0 + 2.0 * frac(sin(st) * 43758.5453123);
}
float perlin(fixed2 st) {
fixed2 p = floor(st);
fixed2 f = frac(st);
fixed2 u = f * f * (3.0 - 2.0 * f);
float v00 = random2(p + fixed2(0, 0));
float v10 = random2(p + fixed2(1, 0));
float v01 = random2(p + fixed2(0, 1));
float v11 = random2(p + fixed2(1, 1));
return lerp(lerp(dot(v00, f - fixed2(0, 0)), dot(v10, f - fixed2(1, 0)), u.x),
lerp(dot(v01, f - fixed2(0, 1)), dot(v11, f - fixed2(1, 1)), u.x),
u.y) + 0.5f;
}
float fBm(fixed2 st)
{
float f = 0;
fixed2 q = st;
for (int i = 1; i <= _Complex; i++) {
f += pow(_Effectivity, i) * perlin(q);
q /= _Effectivity;
}
return f;
}
void surf(Input IN, inout SurfaceOutputStandardSpecular o)
{
fixed2 uv = IN.uv_MainTex;
uv *= _Scale;
uv += _TimeScale.xy * fixed2(_Time.y, _Time.y);
float kiten = fBm(uv);
if (_IsTex == 1) {
o.Albedo = fixed3(kiten, kiten, kiten);
o.Alpha = 1;
//o.Metallic = 0;
//o.Smoothness = 0;
}
else {
//ノーマルの計算
//X方向、Y方向について、傾きを求める
float gradX = fBm(uv + fixed2(1, 0));
float gradY = fBm(uv + fixed2(0, 1));
float3 vecX = float3(1, 0, gradX - kiten);
float3 vecY = float3(0, 1, gradY - kiten);
//傾きの外積を取り、法線を求める
float3 norm = cross(vecX, vecY);
norm = normalize(norm);
float2 grabUV = (IN.screenPos.xy / IN.screenPos.w);
grabUV += norm.rg * _Distortion;
fixed3 grab = tex2D(_GrabTexture, grabUV).rgb;
o.Albedo = grab;
o.Normal = norm;
o.Alpha = _Alpha;
o.Specular = _Spec.rgb;
o.Smoothness = _Smooth;
}
}
ENDCG
}
FallBack "Diffuse"
}
今noteにソースコードを貼ったことをとても後悔しています、noteだと行数とか表示されないのね……まぁQiitaとかに登録するのもダルいのでこのまま進めますが……
噴水ランドでは、下の画像みたいなパラメータで使っていました。
Scaleは波の細かさ、時間のスピードは水面が流れるスピード、歪み具合は屈折の度合いを表しています。他のパラメータもいい感じにいじると楽しいです!
・法線マップを頑張って作る
この辺から詳しい人向けに喋るので、よくわからん人はなんかいい感じに聞き流してください!!!
法線とは、3DCGにおける面から垂直に伸びるベクトルで、その面が向いている方向を示すものです。ノーマルとも言う。
法線は普通に大事な概念で、「法線と光の向きの内積が-1になれば、面は光に対してほぼ直交してると判断できる」みたいな感じでライティングの計算で欠かせない存在です。このシェーダーでは法線をグチャグチャに捻じ曲げることで、うねってる水面を表現しています。(水面ってたぶん凸凹していていろんな方向を向いてるものだと思います)
水面の背後にある画像を取得→法線を歪めて屈折してるっぽく見せる という手法はこの記事(http://tsumikiseisaku.com/blog/shader-tutorial-refraction/)を参考にさせてもらってます。記事では法線の元になるテクスチャを別で用意していますが、自分のシェーダーでは法線をノイズ関数を元に計算しており、法線テクスチャなどは特に必要ないようにしています。
法線の元になっているのは、After Effectsを使っている人ならご存じフラクタルノイズです(fBm関数がそれにあたります)。フラクタルノイズはランダムだけど見え方がなんだかいい感じなので、映像制作で煙などの流体の表現に用いられたりします(画像みたいなやつ)。
フラクタルノイズの生成方法は(https://nn-hokuson.hatenablog.com/entry/2017/01/27/195659)からいただいています。
記事ではフラクタルノイズの関数はその座標における輝度(色)を決めるものになっていますが、自分は輝度を「Z座標の高さ」と捉えることで、なんとか法線を割り出そうとしました。図解するとこんな感じ。
①起点となる座標、X軸における隣の座標、Y軸における隣の座標それぞれについてfBm = Z座標の高さを求める(kiten, gradX, gradY)
float kiten = fBm(uv);
float gradX = fBm(uv + fixed2(1, 0));
float gradY = fBm(uv + fixed2(0, 1));
②X軸方向、Y軸方向におけるZ座標の傾きのベクトルを求める(vecX, vecY)
float3 vecX = float3(1, 0, gradX - kiten);
float3 vecY = float3(0, 1, gradY - kiten);
③X軸方向、Y軸方向の傾きベクトルの外積を取ることで、2つのベクトルに対して垂直なベクトル = 法線が得られる(norm)、ついでに正規化(長さを1にする)
float3 norm = cross(vecX, vecY);
norm = normalize(norm);
みたいな仕組みになっています。伝わります???????????(自分でもうまく言語化できてない……)これで伝わらなかったら僕が悪いです……
とまぁ上記のような流れで、フラクタルノイズを元にした波っぽい複雑な法線が作れます。(フラクタルノイズの様子と生成される法線を比較するとこんな感じ)
(元になっているフラクタルノイズ)
(フラクタルノイズから生成した法線)
・水面に光沢を付ける
光沢を付けるとさらに水面っぽい!という発想は(http://sasanon.hatenablog.jp/entry/2018/10/24/013403)からいただきました。この記事のコードをそのままパクる手も無くは無かったんですが、何度読んでも「???????????????????????????????????????????????????????????????????????」でした。頂点シェーダー何もわからん!!!!!!!!
理解できないものをそのままパクっていくのもなんだか嫌だったので色々探索してみると、Unityのシェーダーのドキュメント(https://docs.unity3d.com/ja/2019.4/Manual/SL-SurfaceShaders.html)にこんな記述が。
サーフェイスシェーダーはデフォルトだと前者のSurfaceOutputStandardというやつが使われておりあまり光沢感などがないのですが、もしかして後者の「SurfaceOutputStandardSpecular」とかいうやつを使えば光沢を足せるのでは???と思って試してみたらビンゴでした。上記のコードで言うところの29行目の部分「 #pragma surface surf StandardSpecular alpha:fade」とか87行目の部分「void surf(Input IN, inout SurfaceOutputStandardSpecular o)」とかでちゃんと指定すると使えるようになるっぽいです。
上記の手続きを経て完成したのがこちらです!!!(何度でも貼る。)
・まとめ
GrabPassなどを用いた屈折表現、フラクタルノイズによる法線の生成、SurfaceOutputStandardSpecularを使った光沢感の追加によって水面っぽいシェーダーを作りました!shaderlabのみで完結しているので、C#スクリプトの使えないclusterでも余裕で運用できました~~~、とりあえず水面が欲しい!!!という人はぜひ!
ここまで読んでいただきありがとうございました!!
最後に参考になった・ヒントになったサイトを貼りなおしておきます。マジ感謝。
Unity シェーダーチュートリアル 屈折表現 - Tsumiki Tech Times
http://tsumikiseisaku.com/blog/shader-tutorial-refraction/
【Unityシェーダ入門】シェーダで作るノイズ5種盛り - おもちゃラボ
https://nn-hokuson.hatenablog.com/entry/2017/01/27/195659
ノイズで背景を歪ませるシェーダーをつくる - ささみ雑記帳 http://sasanon.hatenablog.jp/entry/2018/10/24/013403
サーフェスシェーダーの記述 - unity
https://docs.unity3d.com/ja/2019.4/Manual/SL-SurfaceShaders.html
ここまで読んだ芸工生のみなさ~~~~~~~~ん!!!!!!!!!!これくらいのクオリティで大丈夫だと思うのでなんか記事書いてカレンダーに登録しような!!!!!!!!!!!!!!!!!!
芸工生 Advent Calendar 2020のページ → https://adventar.org/calendars/5827