Compute Shaderについて

  • 15
    いいね
  • 0
    コメント

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のパッチ

一番シンプルと思われる構成のパッチがこれです。関係があるものは同じ色でマーキングしてあります

00.jpg

水色: ユーザー定義変数

ソースコード側で定義した変数は自動的にノードのピンができてパッチ側からいじれるようになる。
この場合だとLFOで定期的にランダムな値を入力している。

紫色: Dispatchernumthreadsの関係

DispatcherThread 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とバッファサイズの関係

RendererElement Countの数はRenderer側で用意されるバッファサイズに相当する。

なので、ReadBackで読んできた値も10要素のスプレッドになっている。

緑: StrideBACKBUFFERに指定した型のサイズ

vvvvでは、BACKBUFFERのセマンティクスをつけたやつが出力されるバッファになる。
BACKBUFFERはソースコード中で一個でなくてはいけない

また、RendererStrideには出力する型のバイト数を指定しないといけない。例えば:

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とか、中途半端な数分のループを走らせたい時には別途何回処理を走らせたいかの変数をシェーダーに入れてオーバーランしないような処理を入れるのが定石らしい。

01.jpg

緑色: Countを追加

新しく変数としてCountを追加して、SV_DispatchThreadIDの値がそれより大きければ何もしないでリターンする処理を入れている。

この一文によって安全でないメモリ領域に書き込む(=バッファオーバーラン)を回避して変なバグを生まないようにしている。

黄色: Dispacher1Dを追加

既に説明したようにCompute Shaderの処理は(Dispacherで指定した数) * (numthreadsで指定した数) 分走ることになるので、Countで指定した数より十分に大きい回数の処理を行うためにはグループサイズをどれぐらいにしたらいいのか? の計算をここのサブパッチでしている。

赤: ReadBackの結果

Countで指定した数ぶんの処理が走って、スプレッドの数もそれと同じになっている所に注目

とりあえず、これで任意の回数の処理を走らせることができる。

もちろん毎回Countdtid.xの比較とif分岐が計算される事になるので汎用性は高まったが効率は悪くなっていることに注意。

設計的に、一回の処理でnumthreadsで指定した回数の処理が走っても問題ない事にできる場合はそちらの方が効率がいいはず

RWByteAddressBufferを使う

今までは簡単のためRWStructuredBufferを使っていたが、実はこの型はCompute ShaderとPixel Shaderでしか使えない。

追記
よくよく考えたら StructuredBuffer を使うと全部のシェーダーからStructuredBuffer<T>の形でデータ読めますね?それを使ったら以下の内容もっと簡単にできるんだけど、、
RWByteAddressBufferもたまに使わざるを得なくなるのでとりあえず残しておきます!
/追記

RWStructuredBuffer

Vertex ShaderやGeometry ShaderでCompute Shaderで計算した結果を使いたい場合は、RWStructuredBufferよりもプリミティブな型であるRWByteAddressBufferを使う必要がある。

02.jpg

緑色: RWByteAddressBufferを使うための変更

Renderer (DX11 Buffer)を、Renderer (DX11 Buffer.Raw)に変更する。今までRWStructuredBufferを使う場合ははStrideCountを入れていたが、ここではStride * Countをした値を確保すべきバイトサイズとして設定する。

ソースコードでRWStructuredBuffer<T>を指定していた所をRWByteAddressBufferに変更。ここで型の指定子がなくなっていることに注意 → C++で言う所のstd::vector<T>malloc等で確保した単純なバイト列の違いのようなかんじ

また、値の読み書き時に今まではOutput[N]のようにインデックスを指定していた所をLoad, Storeなどのメソッドに変更する必要がある。

それぞれバイトオフセットを指定するんだけど、uint型しか扱えない事になっているので浮動小数点を使いたい時はそれぞれasfloatasintasuintなどで型変換する。

詳しくはこちら → RWByteAddressBuffer

とりあえずそのあたりの変更によってRWByteAddressBufferを出力することができるようになる。また、IOBoxなどで内容を確認する時はReadBack (DX11.Buffer Raw)を使う。

VertexShaderでRWByteAddressBufferを使う

03.jpg

出力したRWByteAddressBufferGeomFXに入れてジオメトリを生成する。

入力のジオメトリとしてNullGeometry (DX11.Drawer)を指定して、バーテックスシェーダーの入力値としてSV_VertexIDをインデックスとして使う。

上の画像はSV_VertexIDのインデックスのメモリをLoadで読んできてジオメトリのY軸のデータとして使っているという例です。

まとめ

最後心が折れてだいぶ駆け足になってしまいました。。

別段これだけならばVertex Shaderだけでも全然いけるのですが、、

毎フレームちょっとづつ値を変化させるシミュレーションのような処理など、今までのシェーダーでは結構ややこしいことをしないとできなかったタイプの処理がサクっとできるので、Compute Shader、楽しいですよ…!

今回紹介していたパッチなど、こちらです → https://cl.ly/3L3l072d213H

興味が出てきた方は、このあたりも参考にしてください!↓
Instance Noodles
VVVV.GPUParticle

この投稿は vvvv Advent Calendar 201623日目の記事です。