Unityでシンプルな波シミュレーション 第1回 単一の正弦波で頂点シェーダを動かす
概要
前回:
GPU Gems Chapter 1を参考に、波のシミュレーションをUnityで実装する。
今回は、ひとまず頂点シェーダが動くところまでを目標にする。そのため波のモデルはシンプルに、正弦波1つだけにする。
波モデルの計算式
理論
まず、参考資料の正弦波の式を見ていく。今は複数の波を考えないので
まず左辺を見る。計算結果の値は「高さ」であり、ある時刻の特定の座標の高さを求める式だ。
-
: 頂点の2次元座標x,y -
: 時刻t -
: 計算された頂点の高さW(x, y, t)
右辺についても見ていく。
-
: 波の振幅(amplitude)A -
: 波の向き(direction)\bold{D} -
: 波の振動数(frequency)?w = 2/L -
: 波の速さ?\varphi = Speed \times 2/L
高校物理あたりで習う波の基本式と乖離していてやりづらいので、それらしい形式に修正する。
-
: 波の角波数k = 2\pi/L -
: 波の波長(length)L
-
-
: 波の角振動数\omega = 2\pi/T = 2\pi\cdot v/L -
: 波の周期T = L/v -
: 波の速さv
-
また、時間項はマイナスにするのが自然である。
以後、波の式は次のものを使う。
実質、文字の種類が変わっただけなので法線の計算もほぼ流用できる。ただしUnityはy軸が上方向なので、その点だけ留意して調整する。
として、
とかける。
暗黙に使っているが、頂点座標や波の方向のxy座標はあくまで波の平面におけるローカル座標としてのものである。(本来はx, zとするべき)
hlslの実装
まずは高さの計算から実装する。
基本的に上記の数式をそのままhlsl形式に変換していくが、Custom Fuctionのフォーマットに従う必要がある。
上記のページに載っているサンプルコードを参考にする。具体的に参考にした部分は以下。
-
UNITY_SHADER_NO_UPGRAD: Unityによる自動アップグレードを無効化する- https://docs.unity3d.com/ja/2019.4/Manual/SL-BuiltinMacros.html
- 実は実装時見落としていて、執筆時に確認して気づいた。
- インクルードガード(
#ifndef/#endif)- なくても直ちに致命的な問題にはならなさそうだが、一応書いておく
- ノードの出力は
outで指定する -
<Function>_floatという名前で定義し、ShaderGraph側では<Function>で呼び出す
コードは次のようになった。
//UNITY_SHADER_NO_UPGRADE
#ifndef DEFINE_WAVE_FUNCTION
#define DEFINE_WAVE_FUNCTION
float WaveHeightInner(float2 vertexPos, float time, float waveLength, float waveAmp, float waveSpeed, float2 waveDir)
{
float k = 2.0 * PI / waveLength;
float omega = waveSpeed * k;
waveDir = normalize(waveDir);
return waveAmp * sin(dot(waveDir, vertexPos) * k - time * omega);
}
void WaveHeight_float(float2 vertexPos, float time, float waveLength, float waveAmp, float waveSpeed, float2 waveDir, out float height)
{
height = WaveHeightInner(vertexPos, time, waveLength, waveAmp, waveSpeed, waveDir);
}
#endif // DEFINE_WAVE_FUNCTION
なおPIはURPで定義済みらしく、コードエディタ上では未定義の警告が出るものの、使えた。逆に自前で定義してしまうと2重定義になりエラーになる。
Unity上での実装
高さ(頂点移動)のみ
ShaderGraphを作って次のようにグラフを作成する。Custom Functionノードには上で作成した関数を設定する。

シーン上に平面のメッシュを配置する。Planeを作成して、メッシュを前回作ったものに、マテリアルを上記のShaderGraphのものに変更すればよい。


シーンを再生すると、このように頂点が波打つ様子を確認できる。この時点ではまだ法線を更新していないので、ライティングは若干不自然である。
法線計算を追加
続いて法線も実装してしまう。やることは同様にhlslファイルに法線の計算式を追加し、ShaderGraph上でNormalの出力ノードにつなぐだけである。
float3 WaveNormalInner(float2 vertexPos, float time, float waveLength, float waveAmp, float waveSpeed, float2 waveDir)
{
float k = 2.0 * PI / waveLength;
float omega = waveSpeed * k;
waveDir = normalize(waveDir);
float c = k * waveAmp * cos(dot(waveDir, vertexPos) * k - time * omega);
return float3(-c * waveDir.x, 1, -c * waveDir.y);
}
void WaveNormal_float(float2 vertexPos, float time, float waveLength, float waveAmp, float waveSpeed, float2 waveDir, out float3 normal)
{
normal = WaveNormalInner(vertexPos, time, waveLength, waveAmp, waveSpeed, waveDir);
normal = normalize(normal);
}

以上が法線計算のhlslコードおよびShaderGraphである。

陰影が適切についてより自然な見た目になった。(上のShaderGraphの画像だとwaveDirが(1,1)になっているが、比較のため映像は最初の映像と同じ(1,0)にして撮っている)
ShaderGraph上でpositionやwaveLengthなどの入力パラメータが別れていて煩雑だったので、最後にノードの接続を整理したうえで全体は次のようになった。

補遺(ShaderGraph周りで困ったこと)
- 何らかのクソデカウインドウが常に表示されていて邪魔すぎる

右上のボタンを押すことで表示状態をトグルすることができる。

- これはどうやって出す?

辺上でダブルクリックすると出る。もしくは、右クリックで「Add Redirect Node」を選択。
- ShaderGraphを保存するたびに次のエラーが出る。(動作に影響はない)
Shader error in 'Shader Graphs/hidden/preview/Combine_1cbb378da0c742759f116645f8a61db5': syntax error: unexpected float constant at Assets/Shader/Wave/Include/WaveFunction.hlsl(4) (on d3d11)
Shader error in 'Shader Graphs/hidden/preview/Add_ea8cd04e0d504c9a821ec6a3dfd98b7b': syntax error: unexpected float constant at Assets/Shader/Wave/Include/WaveFunction.hlsl(4) (on d3d11)
確かに最初のコードでは4行目に自前のconst floatでPIを定義していたが、それだとうまくいかなかったので消した。が、消したあとも同じエラーメッセージが出続けていて気持ち悪い。
公式フォーラムにも同様っぽい事象が報告されているものの、特に役に立つ回答はなかった。
最終的に、プロジェクトを開き直すと以降出なくなった。
おわり
波モデルの数式をシェーダコードに落とし込み、Unity上で表示することができた。次回はコードを拡張して複数の波の足し合わせを表現できるようにする。また、ShaderGraph上で高さと法線のCustom Functionが別になっていることでノードの構造が冗長になってしまったので、これも合わせて解決する。
Discussion