投稿日:2024/11/18

インダイレクト描画への道 part 3

   

おはようございます。
エンジニアの yamamoto-mo です。

急に寒くなりましたね。
この間まで半袖だったのに、今は長袖どころか上着まで必要になりました。
今年の寒暖差は常軌を逸している感じがします…

さてさて本題ですが、
前回の続きで「カリング計算を GPU で行う」をやります。

前々回のインスタンシング描画と、前回のコンピュートシェーダを組み合わせて、
・カリングチェックを GPU で行う
・チェックを通過したインスタンスを一度の Draw Call で全部描画する
の実現を今回のゴールとしましょう。
大量のオブジェクトを可能な限り早く描画出来る 事を目指します。

また、せっかくなのでちゃんとしたフラスタムカリングも用意してみました。
(解説はまた別の機会に…)

 

サンプルプログラム

早速ですがサンプルはこちらです。
常套句になっていて恐縮ですが、いつも通り適当ですいません…

32768 個のインスタンスを生成し、
フラスタムカリングで描画可能なインスタンスを選別して描画しています。
デフォルトでは GPU でカリングしており、
1 キーを押下している間だけ CPU でカリングします。

 

実装の流れ

実装の流れですが…実はすでにある程度説明済みだったりします。
以前のブログ記事で挙げた内容とほぼ同じです。

事前準備

・各インスタンスの表示座標や大きさ等の情報が入っている インスタンス配列 を用意する
・描画するインスタンス数を保持する カウンター を用意する
・描画するインスタンス情報が入っている配列の添え字リストとして 描画インスタンス番号リスト を用意する
・カリングに利用する フラスタム情報 を用意する

コンピュートシェーダによるカリング

インスタンス配列フラスタム情報 からカリングチェックする
・描画するインスタンスの総数を カウンターバッファ に加算する
・描画するインスタンス情報の添え字を 描画インスタンス番号リスト に追加する
・コンピュート処理が完了するまで CPU を待機させた後、 カウンター の値を取得する

描画シェーダによるインスタンシング

・Draw Call で カウンター の値を引数に指定する
インスタンス配列 のリソースの状態遷移を行う
・インスタンス ID と 描画インスタンス番号リスト インスタンス配列 を組み合わせて情報を取得し頂点処理する

では順にみていきましょう。

 

事前準備

まずは 事前準備 からです。
カリングや描画に必要な各情報を GPU で利用できるように準備を整えます。

インスタンス配列
サンプルでは instanceData が該当します。
サンプルではインスタンスは移動したりしない為、
初期化で全情報を埋めており、計算や描画でそのまま利用しています。

カウンター
サンプルでは drawInstanceCount が該当します。
描画する数のみなので、int 型で用意し、
コンピュートシェーダで値が加算されています。

描画インスタンス番号リスト
サンプルでは drawInstanceIndex が該当します。
インスタンス配列内にある「描画可能なインスタンス」の添え字が複数入っています。
ピンとこない方は、インダイレクト描画への道 part 1をご確認頂ければと思います。

フラスタム情報
サンプルでは frustumData が該当します。
カメラは移動しないので、初期化で値を決めた後は更新せずに利用しています。
また、フラスタムカリングが効いている事を確認しやすいように、
若干画角を狭めた範囲でフラスタムを作成しています。

 

コンピュートシェーダによるカリング

次に コンピュートシェーダによるカリング です。
まずは各情報をコンピュートシェーダから参照、更新できるように設定します。

インスタンス配列フラスタム情報 からカリングチェックする
ここが該当の処理です。
解説は省きますが、各インスタンスの座標とフラスタム情報からカリングを行います。
描画可能であれば visible には true が入ります。

・ 描画するインスタンスの総数を カウンター に加算する
ここが該当の処理です。
注意点として drawInstanceCount は普通にインクリメントできません。
GPU は並列で処理を行う為、全体で共有している変数に対して同時にインクリメントしてしまうと、
意図しない結果になる事が起こりえます。
これは基本回避しなければなりません。

・ 描画するインスタンス情報の添え字を 描画インスタンス番号リスト に追加する
ここが該当の処理です。
スレッド ID からインスタンス配列の添え字を計算して、
その添え字が指すインスタンスを描画対象として設定しています。

・ コンピュート処理が完了するまで CPU を待機させた後、カウンター の値を取得する
ここが該当の処理です。
GPU で更新した カウンター の値は DrawIndexedInstanced の引数で利用します。
その為 CPU で更新結果を受け取る必要があるのですが、
GPU の処理が終わる前だと、更新途中であるため正しい値が取得できません。

 

描画シェーダによるインスタンシング

最後 描画シェーダによるインスタンシング です。
ここはインダイレクト描画への道 part 1とほぼ同じなので、
さらっと行きましょう。
まずは各情報を描画シェーダから参照できるように設定します。

・ Draw Call で カウンター の値を引数に指定する
ここです。
前述でも述べた通り、値を指定しているだけですね。

描画インスタンス番号リスト のリソースの状態遷移を行う
ここここです。
drawInstanceIndex はコンピュートシェーダでは UAV で利用しますが、
描画シェーダでは参照のみなので SRV で利用します。
リソースを利用する際は「リソースの用途」を指定し、
その用途で使える状態に変更する必要があります。
ある処理で利用中のリソースを、違う用途で同時に利用してしまう事を避ける為です。
必要に応じて GPU 処理が待機する事もありますが、恐らく今回の例では発生しません。

・ インスタンス ID と 描画インスタンス番号リストインスタンス配列 を組み合わせて情報を取得し頂点処理する
この部分です。
頂点情報の取得や計算に、各情報を利用しているのが見て取れるかと思います。

 

結果

さてさて結果ですが、自分の環境だと
コンソールに表示されている更新タグの処理負荷が
・CPU カリング:約 3.2 ms
・GPU カリング:約 0.6 ms

になりました。
GPU を利用すると約 1/5 の処理時間で終わってます
凄いですね。

勿論 GPU 側の負荷がどの程度上がっているのかを確認し、
バランスを見て結果を判断するべきではありますが、
CPU 側の処理時間を見る限り効果は高いと思います。

といっても… CPU カリングはメインスレッド一本でやっているので、
比較対象としてフェアじゃない気もしますが…まぁ…はい…w

 

part 4 へ

という事で
・カリングチェックを GPU で行う
・チェックを通過したインスタンスを一度の Draw Call で全部描画する

をやってみました。
結構インダイレクト描画っぽい形になってきましたね。

…なんですが、これはまだ「インダイレクト描画っぽい」止まりであり、
ちゃんとしたインダイレクト描画に至っていません
というのも、インダイレクト描画と呼ぶためには、
CPU と GPU の行き来を減らす必要があります。

特に
> ・ コンピュート処理が完了するまで CPU を待機させた後、カウンターバッファの値を取得する

が邪魔なんですよね。
というわけで、次は「ちゃんとしたインダイレクト描画をやる」になります。

サンプルプログラム…
そろそろ「気晴らしも兼ねて短時間でささっと作る」範囲で出来なくなってきている気がする…w

 

免責事項

掲載された内容によって生じた損害等に関して、
弊社は一切の責任を負いかねますので、予めご了承ください。