〰️

Unityでシンプルな波シミュレーション 第1回 単一の正弦波で頂点シェーダを動かす

に公開

概要

前回:

https://zenn.dev/kakilemon/articles/9c757c05e1d745

https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models

GPU Gems Chapter 1を参考に、波のシミュレーションをUnityで実装する。

今回は、ひとまず頂点シェーダが動くところまでを目標にする。そのため波のモデルはシンプルに、正弦波1つだけにする。

波モデルの計算式

理論

まず、参考資料の正弦波の式を見ていく。今は複数の波を考えないのでiは省略する。このような式になっていた。

W(x, y, t) = A \times \sin\left(\bold{D}\cdot \left(x,y\right)\times w + t \times \varphi\right)

まず左辺を見る。計算結果の値は「高さ」であり、ある時刻の特定の座標の高さを求める式だ。

  • x,y: 頂点の2次元座標
  • t: 時刻
  • W(x, y, t): 計算された頂点の高さ

右辺についても見ていく。

  • A: 波の振幅(amplitude)
  • \bold{D}: 波の向き(direction)
  • w = 2/L: 波の振動数(frequency)?
  • \varphi = Speed \times 2/L: 波の速さ?

高校物理あたりで習う波の基本式と乖離していてやりづらいので、それらしい形式に修正する。

w\varphiの代わりにk\omegaを導入する。

  • k = 2\pi/L: 波の角波数
    • L: 波の波長(length)
  • \omega = 2\pi/T = 2\pi\cdot v/L: 波の角振動数
    • T = L/v: 波の周期
    • v: 波の速さ

また、時間項はマイナスにするのが自然である。

以後、波の式は次のものを使う。

W(x, y, t) = A \times \sin\left(\bold{D}\cdot \left(x,y\right)\times k - t \times \omega\right)

実質、文字の種類が変わっただけなので法線の計算もほぼ流用できる。ただしUnityはy軸が上方向なので、その点だけ留意して調整する。

C = k\times A\times\cos\left(\bold{D}\cdot(x,y)\times k-t\times\omega\right)

として、

\bold{N} = (-C\times \bold{D}.x, 1, -C\times \bold{D}.y)

とかける。

暗黙に使っているが、頂点座標や波の方向のxy座標はあくまで波の平面におけるローカル座標としてのものである。(本来はx, zとするべき)

hlslの実装

まずは高さの計算から実装する。

基本的に上記の数式をそのままhlsl形式に変換していくが、Custom Fuctionのフォーマットに従う必要がある。

https://docs.unity3d.com/ja/Packages/com.unity.shadergraph@10.0/manual/Custom-Function-Node.html

上記のページに載っているサンプルコードを参考にする。具体的に参考にした部分は以下。

  • UNITY_SHADER_NO_UPGRAD: Unityによる自動アップグレードを無効化する
  • インクルードガード(#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周りで困ったこと)

  1. 何らかのクソデカウインドウが常に表示されていて邪魔すぎる

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

  1. これはどうやって出す?

辺上でダブルクリックすると出る。もしくは、右クリックで「Add Redirect Node」を選択。

  1. 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 floatPIを定義していたが、それだとうまくいかなかったので消した。が、消したあとも同じエラーメッセージが出続けていて気持ち悪い。

公式フォーラムにも同様っぽい事象が報告されているものの、特に役に立つ回答はなかった。

https://discussions.unity.com/t/where-to-put-static-consts-in-urp-shader-file/844879

https://discussions.unity.com/t/my-shader-graph-with-custom-functions-throws-errors-warnings/367494

最終的に、プロジェクトを開き直すと以降出なくなった。

おわり

波モデルの数式をシェーダコードに落とし込み、Unity上で表示することができた。次回はコードを拡張して複数の波の足し合わせを表現できるようにする。また、ShaderGraph上で高さと法線のCustom Functionが別になっていることでノードの構造が冗長になってしまったので、これも合わせて解決する。

Discussion