水面を作ってみた
はじめに
サンプルは上のリンクから。
こちらで紹介されている波紋シェーダーが美しいと思ったので、似たようなものを実装してみました。 最近イカやゼルダやゼノブレイドやHorizon Zero Dawnやモンハンやらで忙しくてLOST SPHEAR買えてないので今度買います。
実装した水面シェーダーはこんな感じになりました。 動画の方は低画質だと見辛いので、設定で画質をHDにすることを推奨します。
足の動きで波を発生させているのですが、動きの量に応じて発生する波の強弱を変える等の調整をすればもっと良くなりそうな気がします。
実装方法
「水面 シェーダー」とかでググるといっぱい出てくると思うので、概要だけメモっておきます。 特に、Unite2017の講演の『スマートフォンでどこまでできる?3Dゲームをぐりぐり動かすテクニック講座』を見るだけで実装できると思います。
実装手順としては
- 反射をレンダリングするカメラを用意する
- 反射を投影しつつ法線に応じて歪めるシェーダーを書く(床面に適応させる)
- 波動方程式を解いて法線としてシェーダーで読めるようにする
- 波動方程式の入力を足の動きに合わせて与えるようにする
といった感じです。
反射をレンダリングするカメラを用意
こんな感じでカメラを用意してやります。上のカメラアイコンの位置にメインカメラを、下のカメラアイコンの位置に反射用カメラを配置しています。 メインカメラの位置は動的に変わる可能性があるので、反射用カメラも反射を描画する位置に動的に移動するようスクリプト等で制御する必要があります。
反射を投影しつつ法線に応じて歪めるシェーダーを書く(床面に適応させる)
DepthShadowの描画の応用です。要するに投影テクスチャマッピングを施します。 詳細は別記事にて書く(次の記事で書きたい)ので、今回は実装のみ簡単に書いておきます。
投影を行う場合、投影する側とされる側で空間を統一して、投影位置を決定する必要があります。 反射で描画されるそれぞれのオブジェクトは反射用カメラの変換行列を用いて変換され、レンダリングされます。 次に、メインカメラで水面のオブジェクトを描画する際に、頂点を反射用カメラの変換行列を用いて変換します。 このProjection空間に変換された同次座標系に属する頂点を通常座標に直し、テクスチャ座標に変換すれば、実質的に反射用カメラでレンダリングされたオブジェクトと同じ空間に属することになります。 同一空間上の場所を得ることができれば、その座標のピクセルカラーをフェッチして描画に使うのみです。
struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float4 ref : TEXCOORD1; }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _RefTex; float4x4 _RefVP; float4x4 _RefW; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.ref = mul(_RefVP, mul(_RefW, v.vertex)); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { return tex2D(_RefTex, i.ref.xy / i.ref.w * 0.5 + 0.5); }
コードは反射を投影するシェーダーの一部です。_RefTex
には反射を描画したテクスチャを、_RefVP
には反射用カメラのVP行列を入力しています。_RefW
は反射用オブジェクトのローカル変換行列です。
頂点シェーダーで_RefVP
を用いて頂点を反射用カメラのProjection空間へと座標変換し、フラグメントシェーダーで通常座標変換 + テクスチャ座標への変換をしています。
これで、カメラの移動や反射用オブジェクトの移動・回転・拡縮があっても違和感のない投影が行えます。
で、あとは法線マップを参照して歪ませるコードを足していきます。
float2 offset = bump * _BumpAmt * _RefTex_TexelSize.xy; i.ref.xy = offset * i.ref.z + i.ref.xy + _UVOffset.xy;
Unity標準のガラスシェーダーを参考にしました。_BumpAmt
は外部から与える影響量の係数、_UVOffset
は外部から与えるUV座標調整用の変数です。
法線で参照する反射用テクスチャの座標をちょっといじっているだけです。
波動方程式を解いて法線としてシェーダーで読めるようにする
波動方程式を解いて、波の高さをテクスチャで取ってこれるようにします。 詳細は記事上部で紹介したUnite2017の講演資料を参照して下さい。死ぬほどわかりやすいです。
慣れていないと偏微分見ただけで吐き気を催すかもしれませんが、こういった画像処理の場合x,y方向の偏微分は隣接するテクセルの変化量(差)であり、要するにただの引き算です。 時間に関する偏微分も以前のフレームで描画したテクスチャを参照し、変化量を求めるだけです。
記事の頭で貼ったGifの左の赤い部分に注目して下さい。これは波動方程式を解くことでえられた波の高さを格納するテクスチャをデバッグ用に表示しているものです。 赤い理由は、最適化のためR要素のみを持つテクスチャを利用しているためです。
因みに左上の白い部分は波立たせるための入力用テクスチャをデバッグ用に表示したものです。見辛いですが、所々黒い点が入力されています。Gifエンコードの関係上、少しグレーであとが残っちゃてますが本当はグレーのあとは残りません...。
次に、このテクスチャに格納された波の高さ(ハイトマップとして扱う)を法線として解釈し、反射用テクスチャを歪ませるようにします。 ハイトマップから法線マップを算出する際にも実は偏微分を解いています。
以前の記事で紹介した、このハイトマップから法線マップを算出する処理がそのまま使えます。 こうして取ってきた法線を先ほどの水面シェーダーのbumpとして入力してやれば、このプロセスは完成です。
波動方程式の入力を足の動きに合わせて与えるようにする
足にColliderを付け、InkPainterのCollisionPainterを改造したスクリプトを付けました。
GitHub - EsProgram/InkPainter: Texture-Paint on Unity.
InkPainterでペイントを行う場合、今回のケースだと1フレームに複数回、入力用のテクスチャを作るためだけに描画処理が走ることがあります。これは1回の処理で済ませられるよう最適化すべきですが、とりあえず今回は動けばいいので最適化等はしていません。
最後に
こういうものが比較的迷わず作れてしまうくらい資料や環境が整ってるのがUnityの良いところですね。 次はよく使う変換手法について纏められたらと思います。