この記事はvvvv Advent Calendar 2020 12日目の記事です.
今年10月のNODE 20に合わせて、vvvv gamma 2020.3 previewとともに、ついにVL.Strideがpublic previewとして全ユーザーが使用できるようになりました。 VL.Strideによって、Stride Game Engineのさまざまな3DCG関連の機能がvvvv gammaから利用できるようになり、このあたりの不足を理由にvvvv gammaに手を出せていなかった方も入門しやすくなったのではと思います。
ShaderFXとは
そんなVL.Strideの興味深い機能の一つにShaderFXというものがあります。これはその名の通り、シェーダーに関連する機能なのですが、平たく言うとノードベースでカスタムシェーダーが作れる機能になります。 vvvv betaのように、コードで書かれたシェーダーを組み合わせてカスタムのエフェクトを作る…みたいな話ではなく、Stride内のシェーダー言語の機能を利用して、シェーダーコード自体をノードで組み上げることが可能となっています。 UnityのShader GraphやUnreal Engineのマテリアルエディタ、vvvvで言うとFild Tripを辺りを使ったことがある方は想像がつきやすいかもしれません。実際公式でもこの辺りが参考として挙げられています。 これを使えば、vvvv betaではコードが書けなくて、シェーダーを深くいじれなかったという方でも手軽にカスタムシェーダーを作ることができるように…なると思うのですが、結論から言うと、正直現状はpreviewなだけあって、機能不足感は否めず、結局後述の仕組みでちょいちょいコードを書かざるを得ないことが多いと思います。 しかし、例え結局コードを書くにしても、ノードで視覚的に組む利点はある上に、すべてコードで書かなければならないよりはとっかかりやすいと思います。
サンプルを見てみる
実際に見てみます。バージョンはvvvv gamma 2020.3-0104を使用しています いきなり何か作ってみる前に、一応ShaderFXのサンプルがHelp Browserから利用できるのでそちらを見てみます。まず最初に見てみるにはこちらのPatch a Drawing Shaderからがよいと思います。 こちらがパッチの全体図です。ここで作られているシェーダーは、uvを用いてテクスチャをサンプリングし、そこに任意の色を乗算して、その色をそのまま出力するというシンプルなものです。 基本的なところを少し説明すると、シェーダーにはVertex ShaderとPixel Shader(OpenGLとかだとFragment Shaderって名前だったりします)という、基本になる2つのステージがあり、それぞれ頂点ごとの処理と、画面上のピクセルごとの処理を担当しています。 ShaderFXにおいても、基本この2つのステージの処理をノードによって定義していきます。それによって、頂点ごと、ピクセルごとに並列に全く同じプログラムが動くことになります。
Pixel Shaderでは、以前までのステージからの情報を受け取り、それを利用して実際に画面に出力する色を決定します。 これらのプログラムをShaderFXではノードを用いて組んでいくのですが、使用するノードについては、*やTransformなど、見慣れた名前のものもありますが、すべてShaderFX専用のノードであり、Stride.Rendering.ShaderFX名前空間のものを使用する必要があります。 また、そこで扱う型はすべてVar型(こちらは将来的にはGPU<T>という名前になっていきそうです)のジェネリクスになっていて、vvvvのインターフェース上では(GPU上のリソースなので当然と言えば当然ですが)実際の値は確認できず、型の情報とノードの機能のみを基にシェーダーを組み上げていくことになります。 そして、最終的に出力する情報(VertexShaderでは、頂点位置の情報としてのVar<Vector4>、PixelShaderでは出力する色の情報としてのVar<Vector4>)をDrawFXGraphノードのVertex Root、Pixel Rootにそれぞれ入力することで、ノードを基にシェーダーを作ってくれます。 それではここからはもう少し細かく処理を見ていきます。 まずは順番通りにVertexShaderの部分から。VertexShaderのこの部分では、頂点位置の変換を行っています。POSITIONノードによって、ジオメトリの各頂点の位置を取得し、それに対してWorldViewProjectionノードによって取得した行列を乗算することで位置の変換を行っています。 ここでのWorldViewProjectionとは、頂点の位置変換に必要な行列をまとめたもので、各Entityに入力したTransformationを用いた変換を行うWorld行列と、カメラから見た位置へ変換を行うView行列、透視投影変換を行うProjection行列が一つにまとまっています。この辺りはvvvv betaでも他の3D環境でも扱いはほぼ変わりません。 例えば、頂点位置変換を行っているこの部分を飛ばして、POSITIONノードのOutputをそのまま下流に接続してみると、変換が行われず、ビューポートに張り付いた形になると思います。 次にお隣の処理を見ていきます。TEXCOORDノードにより取得したTexture Coordinate、つまりuvの調整を行っています。ここで用いているInノードは定数パラメータを入力するためのノードで、CPU側から固定の値を入力することができます。これをSetTEXCOORDノードを使用して、計算結果をuvに戻しています。 ここで、パッチに書かれたコメントを見ると、
"Modify TEXCOORD semantic"
とあります。Semanticとは、特定の意味付けされた情報のことで、ジオメトリ→VertexShader→PixelShaderなどのシェーダーステージ間の情報の入出力はすべてこのSemanticを介して行われます。
POSITIONノードやTEXCOORDノードの中身を見てみると、Semanticノードを用いて取得したり、SetSemanticノードを使用して、Semanticの値の設定としてuvの更新を行っていることがわかります。もちろんこれらを利用すると、任意の名称で、ユーザーが自分で定義した独自のSemanticを設定することもできます。
そして、これらの計算結果をVertexRootとしてDrawFXGraphノードに入力したいのですが、当のDrwaFXGraphノードにはVertexShader用の入力は1つしかありません。uvの更新を行うには、その前にDoノードを用いて、Beforeの入力にSeTEXCOORDノードOutputを入力することによって、uvの更新の処理を呼び出しています。
このDoノードからは、改めてVar<Vector4>のOutputが来るため、いくつかのDoノードを重ねて、さらに別のSemanticをPixelShaderに向けて設定することもできます。
こうして設定されたSemanticの情報は、ラスタライザによって各ピクセルごとの値に補完され、PixelShaderに入力されます。
ということで、今度はPixelShaderの処理を見ていきます。こちらは見た通り単純で、GPUに対してTextureを入力するTextureVarノードを用いてテクスチャサンプリングを行い、そこにColorInノードで入力した色を乗算して出力しています。
ここまでが、大まかにDrawFXGraphノードを用いた、ShaderFXでのシェーダー作成のサンプルになります。慣れない方は、ネット上のHLSLの解説や、vvvv betaのシェーダーの中身などと比べてみたりすると、より理解が深まると思います。また、ここまで理解できたなら、もう少しAdvancedな面白いサンプルがCompose with Depth Bufferにあります。
こちらは、別シーンでレンダリングしたDepthのテクスチャを用いて板ポリゴンのDepthを上書きするサンプルです。ぱっと見では構造がつかみづらいですが、いろいろとノードを組み替えたりしながら見てみるとわかりやすいと思います。
他にどんなことができそうかは、Stride.Rendering.ShaderFX名前空間に関連ノードが入っているのでそちらを見てみましょう。これらはExperimentalなノードですので、検索欄の左側のExperimentalのスイッチをOnにしないと出てきません。
ノードでPBRのシェーダーを組む
ここまでサンプルとともに、DrawFXGraphノードを用いてカスタムシェーダーを組む手順を見てきました。ですが、ここまで紹介した仕組みだけでは、シェーダー内のほとんどの処理を自ら定義しなければなりません。VertexShaderはともかく、PixelShaderにおいては、せっかくStrideには高品質なPBRシェーダーが含まれているのに…と思ってしまいます。 しかし、実はShaderFXでノードを用いてシェーダーを組みつつ、StrideのPBRシェーダーの恩恵も受けられる仕組みがちゃんと用意されています。 といっても別に実はというほど隠れた機能というわけでもなく、Help BrowserにProcedual Materialというサンプルがあります。 Invokeノードや、Func<T1, T2>型といった新たな見慣れない要素もありますが、注目したい要素は、様々な物理パラメータの設定を基にPBRシェーダーをまとめ上げるPBRMaterialBase(Metallic)ノードです。 こちら、一部Diffuseノードなどが間に挟まったりしてますが。基本的には先ほどまで扱っていたVar<T>を使用して、PBRマテリアルに必要なパラメータを設定できるようになっています。実はこのPBRMaterialBase(Metallic)ノードですが、Baseというだけあって、よく使うPBRMaterialノードの中身に、 PBRマテリアルを作るベースのノードとして、 同名の少しだけ機能が違うノードが使われていたりします。 この仕組みを利用すれば、それこそUnityのShaderGraphなどと同様に、単に色やテクスチャを指定するだけにとどまらない複雑なシェーダーを作ることができます。サンプルでは、頂点座標を基に3Dノイズをサンプリングしているので、uvの継ぎ目のないシームレスな模様が生成できています。
カスタムシェーダーを作る
ここからはちょっとしたカスタムシェーダーを作ってみます。せっかくなので、vvvv betaと比べたりできるように平野さんのYoutubeチャンネルにあるシェーダーのチュートリアルと同じものを作ってみます。 こちらが作ってみたパッチです。残念ながら、法線の取得で若干コードを書かざるを得ませんでした…。この辺りは今後の発展に期待しましょう。 基本のアルゴリズムの解説は先ほどのYoutube動画にお任せします。DotやNormalizeのためのノードがまだないので、プリミティブな計算ノードで泥臭くやっています。 一部コードを書いた部分ですが、GenericComputeNodeの各種ノードを使用してシェーダーを読み込むことで様々な処理を追加できます。ここでは、FromSourceCodeノードを用いてパッチから直接コードを入力してGenericComputeNodeを生成しています。しかし、この方法だと、なぜかシェーダーが見つからないとエラーが出まくるので、おそらくまだ安定していないんだと思います。 代わりに、外部のシェーダーファイルをクラスの名前でFindして読み込む方法を用いてみます。そして代わりの方法を使うついでに、泥臭い処理は一緒にコードでやってしまうことにします(本末転倒気味ですが)。 パッチ(.vlファイル)を保存している場所に、shadersという名前のフォルダを作り、そこに自作のシェーダーファイルを置いて、クラスの名前を指定することで、vvvvが勝手に見つけてきてくれるようになります。 そこでは、.sdsl拡張子のStride独自のシェーダー言語を用います。といっても、ほぼほぼHLSLと同じもので、そこにクラスや継承といった割と高級な機能を追加して拡張したものになっています。こちらも解説したりしたいですが、自分自身まだちゃんと理解できていないうえに、ちょっと現時点ですでに力尽きそうなのでのちの機会に… これをGenericComputeNodeのByNameノードを用いて読み込みます。CreateCompositionノードを使用して外から引数を入れることもできるので、この方法でほとんどの応用が利くと思います。 さらについでに、BufferInノードを用いて、Bufferリソースもノードから入力、取得できるので、Instancingを用いた実装もノードで行ってみました。SV_InstanceIDノードで、そのまま各インスタンスのIDのSemanticが取れるので、GetItemBufferのIndexに入力することで、インスタンスごとに異なる値を取得できます。 GPUインスタンシングを使用すれば、大量のオブジェクト描画でもサクサク動きます。現状でもこのインスタンシングの処理がノードのみで完結できるのは嬉しいですね。
まとめ
後半力尽きてかなり雑になってしまいましたが、とりあえずShaderFXを用いると、vvvv betaではできなかったノードでシェーダーを書くことができるということが伝われば幸いです…! 何度か示したように、VL.Stride自体まだpreviewの段階であり、いろいろと不便なところも多くあります。しかし、そのあたりは、githubのissueで現在も活発に議論が行われているので、今後の発展に期待に期待しましょう! さらに興味のある方はこの辺りも参考にしてみるといろいろと作れるようになると思います。
VL.Stride.ShaderFX
Stride | Shading Language また、今回の私の方で作ったパッチはgithubの方にそのまま放り投げています。こちらではGPUインスタンシングをいくつかの方法で実装してみたものも付属しているので、興味のある方はご確認ください。それでは今回はこれにて!