vvvvのDX11環境で使えるシェーダーの中にはVertex, Geometry, Pixelなどいくつかのコンテクストがありますが、Compute Shaderは、GPUで数値計算をするためのものです。
シェーダーをある程度触ったことのある人ならばわかるとおもいますが、GPUの処理の特徴として単純な計算をひたすら繰り返すのがやたらと高速、というのがあります。
Vertex Shader, Geometry Shader, Pixel Shaderなどもその恩恵をうけてだいぶ高速に色々できるのですが、
Compute Shaderの他にはないユニークな点として、GPU内のメモリの読み書きが簡単にできる、というものがあります。これは例えば以前のフレームの状態を引き継いだシミュレーションのようなプログラムがサクっとできる、ということにつながります
…もっと他にCompute Shaderならではな利点はあるとおもいますが、とりあえずやってみるか~って時に一番わかりやすかったので、こういうパーティクルエンジンを作ってみました。
作ってみての感想として、非常におもしろかったのですが、色々ハマりました。。。
というかそもそも初学者向けの導入の記事が全然みあたらんかったな… というのがあったので、Compute Shaderやってみたいんだけどな… という人がこれまでよりもいいスタートを切れるようにと、この記事を書いています。
できるだけ簡単に書こうと思っていたのですが、もはやかなりハードコアなエリアで簡単には書けないなと思ったので、どうせならとなるべく詳細に、簡潔に書くように努めました。それでは、よろしくどうぞ…
Compute Shaderを使う時に出てくる基本ノード
Renderer (DX11 Buffer XXX)
普通のRenderer (DX11)
やRenderer (DX11 TempTarget)
などと違って、画像ではなく処理したデータをGPUメモリに書き出すためのもの
Dispatcher (DX11.Drawer)
Compute Shaderで計算するにあたって、どのぐらいの並列度で計算するかを決めるノード。多いほど並列度が高くなる(多分)
ReadBack (DX11.Buffer XXX)
計算した結果はGPUのメモリに乗っているので、それをCPUに持ってくるためのノード。CPUにもってこれればIOBoxなどで確認できたりする
一番簡単なCompute Shaderのパッチ
一番シンプルと思われる構成のパッチがこれです。関係があるものは同じ色でマーキングしてあります
水色: ユーザー定義変数
ソースコード側で定義した変数は自動的にノードのピンができてパッチ側からいじれるようになる。
この場合だとLFOで定期的にランダムな値を入力している。
紫色: Dispatcher
とnumthreads
の関係
Dispatcher
のThread X
には1が指定されている。
これはシェーダーファイルの[numthreads(4, 1, 1)]
の部分と関連が深い。
たとえば、Dispatcher
で(1, 1, 1)
が指定されていてソースコードのほうのnumthreads
で(4, 1, 1)
が指定されている場合、
(4 * 1) * (1 * 1) * (1 * 1) => 4スレッド
の処理が行なわれることになる。なのでReadback
で読み出してきた値も4要素のみが更新されている。
SV_DispatchThreadID
のセマンティクスがついた引数には現在処理中のスレッドIDが入ってくるので、それを使ってOutput
にインデクスを指定して値を格納していく
オレンジ: Element Count
とバッファサイズの関係
Renderer
のElement Count
の数はRenderer
側で用意されるバッファサイズに相当する。
なので、ReadBack
で読んできた値も10要素のスプレッドになっている。
緑: Stride
とBACKBUFFER
に指定した型のサイズ
vvvvでは、BACKBUFFER
のセマンティクスをつけたやつが出力されるバッファになる。
BACKBUFFER
はソースコード中で一個でなくてはいけない
また、Renderer
のStride
には出力する型のバイト数を指定しないといけない。例えば:
struct MyStruct {
float3 Position;
float3 Velocity;
float4 Color
};
のようなユーザー定義構造体を指定する場合には、(3 + 3 + 4) * sizeof(float) = 40
なので、40を指定する。(OpenGLのCompute Shaderと違ってアライメントは考慮しなくていいみたい。単純にパックされたサイズでOK)
赤: 実行する処理を指定
ここは見ての通りなのですがTechnique
のピンで実行する処理を指定できます。
同じソースコードに複数の処理を書いておいて、Technique
のピンの指定違いで別の処理を走らせたり、Group
でまとめた複数の処理を一度に走らせるといったこともできます。
任意の回数の処理を走らせる
さきほどの例だと処理の回数がnumthreads
に指定していた4の倍数分でしか処理を走らせられなかった
こういう時に5とか9とか、中途半端な数分のループを走らせたい時には別途何回処理を走らせたいかの変数をシェーダーに入れてオーバーランしないような処理を入れるのが定石らしい。
緑色: Count
を追加
新しく変数としてCount
を追加して、SV_DispatchThreadID
の値がそれより大きければ何もしないでリターンする処理を入れている。
この一文によって安全でないメモリ領域に書き込む(=バッファオーバーラン)を回避して変なバグを生まないようにしている。
黄色: Dispacher1D
を追加
既に説明したようにCompute Shaderの処理は(Dispacher
で指定した数) * (numthreads
で指定した数) 分走ることになるので、Count
で指定した数より十分に大きい回数の処理を行うためにはグループサイズをどれぐらいにしたらいいのか? の計算をここのサブパッチでしている。
赤: ReadBack
の結果
Count
で指定した数ぶんの処理が走って、スプレッドの数もそれと同じになっている所に注目
とりあえず、これで任意の回数の処理を走らせることができる。
もちろん毎回Count
とdtid.x
の比較とif分岐が計算される事になるので汎用性は高まったが効率は悪くなっていることに注意。
設計的に、一回の処理でnumthreads
で指定した回数の処理が走っても問題ない事にできる場合はそちらの方が効率がいいはず
RWByteAddressBuffer
を使う
今までは簡単のためRWStructuredBuffer
を使っていたが、実はこの型はCompute ShaderとPixel Shaderでしか使えない。
追記
よくよく考えたら StructuredBuffer を使うと全部のシェーダーからStructuredBuffer<T>
の形でデータ読めますね?それを使ったら以下の内容もっと簡単にできるんだけど、、
RWByteAddressBuffer
もたまに使わざるを得なくなるのでとりあえず残しておきます!
/追記
Vertex ShaderやGeometry ShaderでCompute Shaderで計算した結果を使いたい場合は、RWStructuredBuffer
よりもプリミティブな型であるRWByteAddressBuffer
を使う必要がある。
緑色: RWByteAddressBuffer
を使うための変更
Renderer (DX11 Buffer)
を、Renderer (DX11 Buffer.Raw)
に変更する。今までRWStructuredBuffer
を使う場合ははStride
とCount
を入れていたが、ここではStride
* Count
をした値を確保すべきバイトサイズとして設定する。
ソースコードでRWStructuredBuffer<T>
を指定していた所をRWByteAddressBuffer
に変更。ここで型の指定子がなくなっていることに注意 → C++で言う所のstd::vector<T>
とmalloc
等で確保した単純なバイト列の違いのようなかんじ
また、値の読み書き時に今まではOutput[N]
のようにインデックスを指定していた所をLoad
, Store
などのメソッドに変更する必要がある。
それぞれバイトオフセットを指定するんだけど、uint
型しか扱えない事になっているので浮動小数点を使いたい時はそれぞれasfloat
、asint
、asuint
などで型変換する。
詳しくはこちら → RWByteAddressBuffer
とりあえずそのあたりの変更によってRWByteAddressBuffer
を出力することができるようになる。また、IOBoxなどで内容を確認する時はReadBack (DX11.Buffer Raw)
を使う。
VertexShaderでRWByteAddressBuffer
を使う
出力したRWByteAddressBuffer
をGeomFX
に入れてジオメトリを生成する。
入力のジオメトリとしてNullGeometry (DX11.Drawer)
を指定して、バーテックスシェーダーの入力値としてSV_VertexID
をインデックスとして使う。
上の画像はSV_VertexID
のインデックスのメモリをLoad
で読んできてジオメトリのY軸のデータとして使っているという例です。
まとめ
最後心が折れてだいぶ駆け足になってしまいました。。
別段これだけならばVertex Shaderだけでも全然いけるのですが、、
毎フレームちょっとづつ値を変化させるシミュレーションのような処理など、今までのシェーダーでは結構ややこしいことをしないとできなかったタイプの処理がサクっとできるので、Compute Shader、楽しいですよ…!
今回紹介していたパッチなど、こちらです → https://cl.ly/3L3l072d213H
興味が出てきた方は、このあたりも参考にしてください!↓
Instance Noodles
VVVV.GPUParticle