【3D/Shader】Vertex Shader(水の表現)

概要

本記事では下の動画のように水のゆらめきを表現できるようなVertex Shaderについて解説します。

<使用ソフト>
Godot Engine:3.5.1

事前準備

水を入れるための入れ物が必要なので下記リンクよりレンガ模様のプールの3Dデータをダウンロードしてください。
簡単な形状なので自前で用意していただいても良いかと思います。

【3D】プールモデルダウンロード - Godot Engine技術情報 (capl.jp)

ノード配置

まずは水の入れ物のプールから配置します。
先ほどのダウンロードモデルまたは自前のモデルをGodotへインポートします。

gltfファイルは×マークになっているかと思いますが、ダブルクリックして新規の継承をクリック。
その後、新規作成されたプールモデルのシーンをそのまま名前をつけて保存すればOK
です。

今後3Dモデルとして配置する場合には今作成したシーンファイルをドラッグ&ドロップで配置してください。
新規でメインのシーンを作成し、シーンノード編集エリアにドラッグ&ドロップでプールのシーンを配置します。

さて、次は水のメッシュを配置していきましょう。
Poolのシーンの中に配置するでも、メインシーンの中でPoolの子ノードとして配置するでもどちらでも構いません。
作り方として、もしプールの中の水の色や高さ等を色々変えたいようなケースであれば子ノード方式の方が良いですね。
水の入ったプールということで一体化したものとして扱う(水は用途に応じて変更しない)ようであればPoolシーン内に配置して、1つのシーンでまとめて管理できるので楽です。
今回は子ノードとして配置する方式で進めます。

ノードの追加から「MeshInstance」ノードを配置、その後インスペクターエリアの設定でMeshを選び「新規CubeMeshを選択して立方体のMeshを作成しましょう。

メインエリアの左上の移動/回転/拡大メニューを使って、水面は上端より少し下に、Meshの大きさはプールの穴と同じか少し大きいくらいにしましょう。

これでノード配置は完了です。

シェーダーマテリアルの作成

ここから水Meshに対して、シェーダーを設定していきます。
今回やりたいことは以下の2つです。

1.水メッシュの表面を水面のように位置/時間によって高さを変える。
2.水のように若干水色に着色しながらも、裏のレンガ模様が見えるように透明にする。

1に関してはVertex Shaderという機能を、2に関しては水の色に対するアルファ値(透明度)の設定で実現します。
どちらもGodot Engine内ではVisual Shaderというエディタで設定できます。
※ShaderScriptでシェーダーをプログラムで書いても良いです。が、後から視覚的に何をしているのか分かりやすいように本サイトではVisual Shaderを推奨します。

まずは、MeshのインスペクターエリアからMeshInstance->Materialで新規ShaderMaterialを作成。

作成されたマテリアルの球をクリックすると詳細メニューが出ますので、Shaderから新規VisualShaderを選択。

すると、下に見慣れないエリアが出てきました。これがVisual Shader編集エリアです。
ここにいろいろと配置して見栄えを作成していきます。

まずは簡単な「2.水のように若干水色に着色しながらも、裏のレンガ模様が見えるように透明にする。」からやっていきましょうか。
「ノードを追加」を選択し、検索ボックスにcolorと入力すると色々と出てきますが、この中で「ColorUniform」というものを配置します。

ColorUniformは色を決めるブロックです。
下のように色の名前はWaterColorとでもして、colorはAlbedoに、alphaはAlphaにColorUniform側の点から出力側の点にドラッグ&ドロップすることで接続しましょう。また、Default Value Enabledはオンにしておいてください。
※これをオンにするとVisualShader側で初期値を設定できます。

これでDefault Valueを調整すると水の色/透明度を設定できます。
適当に水色っぽい色を選択した後、下のAパラメータの調整で透明度を指定します。
Aパラメータが0になるほど透明度は大きくなります。水色の色にもよりますが、だいたい80前後で良いかと思います。

一気に水っぽくなりましたね。もし水面を揺らす必要がない場合にはここまでの作業でOKです。

Vertex Shaderの設定

ここから水面を揺らす設定をしていきます。

まずは頂点の設定モードへの切り替えです。
シェーダー編集エリアの上部にモード選択がありますので、現在のフラグメントから頂点に変更します。
また、別のOutputが出てきましたね。こちらは頂点のメッシュの頂点の位置/色を決める設定です。

さっきもフラグメントで色を決めていたけど頂点も色を決める?と思った人もいるかもしれません。
これについては、レンダリングの説明になるので詳細は省きますが、まず頂点シェーダーが演算されてから、その頂点シェーダーの情報を元にフラグメントシェーダーが演算されて画面に表示されます。
※記事の最後に解説していますので、気になる方はどうぞ。

今回ここで設定する項目はVertex(頂点情報)のみです。
元々のメッシュにある頂点情報を頂点シェーダー内で変換して出力し、メッシュが波打っているように見せる処理をします。

まずは、メッシュが1面あたり4点だけの立方体メッシュだとどうしようもないので、メッシュの頂点数を増やします。
メッシュのインスペクターを開きSubdivideのWidth/Depthの分割数を10にします。
(Heightは不要です)

頂点シェーダーに戻ります。
ノードの追加でInput/VectorDecompose/VectorComposeを追加し、VectorComposeの出力をVertexに繋げてください。これはメッシュの元の頂点のベクター情報をDecomposeでxyzに数値分割し、Composeでベクター情報に統合してOutputに接続しています。
つまりこの段階では入力された頂点情報をそのままOutputに渡しているだけです。笑

次にyを少しだけ変換していきます。Expressionノードを追加します。
その後、入力を追加で2つ入力を追加して、それぞれの変数タイプをScalarにしましょう。

また出力も1つ追加して、その変数タイプもScalarにします。
さらにInputノードを追加して、timeを取得しましょう。そのうえでそれぞれのノードを以下のようにつなぎます。

Expressionの中の設定はそれぞれの入出力の名前を設定して、一番下のボックス内にプログラムを入力します。
今回の例では元々のyの高さに対して、時間情報で変動するサイン波を足し合わせるといった処理です。
このようにすると、液面が上がったり下がったりしますね。

次にxの位置によって液面の高さが変わるようにしましょう。
xの入力を追加し、こちらもsin波にして時間のサイン波と掛け合わせます。(こうすることで±1に正規化できるので後々楽です)
現状水が枠から飛び出ているので、ついでに変数をもう1つ追加して最大値/最小値を制限しましょう。変数はScalarUniformで追加します。(初期値はだいたい0.3くらいで良いでしょう)

そうすると、今度は横揺れの波になります。後もうすこしです。

今度はzの値も揺れのパラメータに追加しましょう。これで水面の揺れの完成です。

ちなみに各サイン波をかけあわせる上の例だとけっこう周期的な水の揺れっぽく見えます。
逆に下のようにサイン波を足し合わせると、ランダム感が出ます。(最初の動画例は足し合わせ方式です)
どちらを使うかはお好みで。

後は、波の大きさや色をインスペクター上で調整して完成です。

シェーダー設定の中でUniform ColorやScalarUniform などUniform系で定義した変数はインスペクター上で調整できるようになるので後から調整するのが楽になります。そのため、こだわるのであれば下記のように全部ScalarUniformからパラメータを設定し、インスペクター上で調整して良い塩梅を探るというやり方が良いかもしれません。

o_y = i_y + Amp/2.0*(sin(amp_x * i_x)*sin(freq_time_x*time)
+sin(amp_z * i_z)*sin(freq_time_z*time));

(脱線話)頂点シェーダーとフラグメントシェーダーについて

頂点シェーダーとフラグメントのシェーダーの違いについて、最初記事の途中で解説していたのですが、操作とは直接関係のない脱線話が延々と続いてしまったので最後に回してきました。笑

頂点シェーダーとフラグメントシェーダーがどのように機能しているかについてちょっと解説します。少し実際のシェーダーで見てみましょうか。
フラグメントシェーダーで設定したColorUniformをコピペして頂点側に貼り付けます。わかりやすいように色を少し濃くしておきましょう。

これだけでは何も変わりません。頂点色は青色で設定していますが、結局フラグメントシェーダー側で水色に設定されているためです。

次に、フラグメントシェーダー側でInputというボックスを使ってMeshに設定されている色情報/Alpha情報を読み込みます。これをフラグメントシェーダーの出力に設定します。

すると、頂点側に設定した水の色に変わりましたね。
頂点カラーはこのようにフラグメント処理の前に各頂点の情報を演算し、フラグメント処理に渡しています。

頂点でもフラグメントでも色や透明度は演算しているなら、どっちでやれば良いのか?という疑問があるかと思います。基本的にはフラグメント側で処理にすることをお勧めします。
理由としてはフラグメント処理の方が、より画面出力側に近く考え方がシンプルだからです。

上記に関してもう一例。
単純な一色であれば頂点でもフラグメントであっても変わりません。ただし、Albedoに設定する色情報が画像になってくると話が違ってきます。

まずは、フラグメントに画像のAlbedoを指定した場合。
単純に面に画像を貼り付けているような処理になっていますね。

次に頂点カラーに画像を設定した場合。
Subdivisionで分割する前のメッシュにこの設定を適用すると、頂点カラーはメッシュの頂点にしか色情報がのってこないのでおかしなことになります。

水面の頂点はまだ4点しかないので当然ですね。メッシュの頂点を縦横10分割すると、若干それっぽくなってきました。

それでも粗いですね。ということで基本的には出来るだけフラグメントシェーダーで実装することをお勧めします。

ただし、例外が2つ。

1つ目はif文などの条件分岐がシェーダーで入る場合です。
フラグメントシェーダーで条件分岐を入れすぎると、パフォーマンスが明らかに低下します。

というのも、頂点シェーダーの演算回数は頂点の数のみです。
それに対して、フラグメントシェーダーの演算回数は画面のピクセル数分になります。(そのためピクセルシェーダーとも言います)
小さい600×400の画面でも毎秒240,000回×fps分の演算が入るわけなので、これにif文等を入れたらどうなるかは察しがつくかと思います。

昨今GPUの処理性能も上がってはいますが、GPUはif文の処理がとても苦手です。
GPUは同じ処理を同じ法則に従って並列処理することに特化したものなので、if文が入ると急激にGPUの有効実行率が低下します。(気になる方は「GPU Warp処理」などで調べてみてください)

上記の理由からフラグメントシェーダーにif文を入れるくらいであれば、いっそMeshの頂点数を増やして頂点シェーダーで演算したものをフラグメントシェーダーに渡した方が結果的に軽くなることも多いです。
※途中にジオメトリシェーダーやラスタライザーなど別の処理もあるので、一概には言えないですが。

2つ目は色以外の頂点シェーダーの情報(位置など)を元に描画したい場合です。
例えば本記事の水のシェーダーをさらに突き詰めると、水面の法線の方向が水面と並行に近づくと入射光が全反射しますので、頂点の法線方向を演算し白く光るような処理を入れると、かなりリアルな水面になるはずです。(やったことないですけど)
そのような頂点の情報を使う際にもいったん頂点シェーダーで演算した方が効率が良いですね。

一般に公開や販売されているシェーダーで大したことしていないのに(失礼)、やけに重いなというシェーダーのスクリプトを見てみると、フラグメントシェーダーに条件分岐が大量に入っていた、、、なんて怖い話もよくあることです。

相反する結論にはなってしまいますが、自分で作る場合には出来るだけフラグメントシェーダーで作りましょう。
有料シェーダーを買う場合にはフラグメントシェーダーが原因でパフォーマンス性能が悪いこともありますので、評価等を良くチェックすることをお勧めします。

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です