24/10/27 up
前回はCPUでのLOD選択について解説しました。
今回はそれをGPUでやってみようという話です。
理論的な部分は前回そのままなので特に解説しません。
まずは普通にCompute Shaderで実装する方法を考えましょう。
1つの方法としてはLODレベルごとにIndirect Dispatchを行う方法です。この方法は今回は実装していません。
前回で言えばリニアにツリーをトラバースしする方法となります。
当然GPUでの実装ですので、並列性を考慮する必要があります。
やり方は単純ですが面倒です。
まず、最初のDispatchはルートメッシュレットに対して行います。つまり、最も高いLODレベルに対して行います。
最初のLOD選択では普通に可視性を判断し、描画する場合はそのままIndirect Draw変数に書き込みます。
描画しないと判断した場合は子メッシュレットのインデックスとIndirect Dispathの変数へ書き込みます。
2番目のLODからは常にIndirect Dispathとなります。これを LOD 0 まで行っていきます。
この方法の問題点は各LODでの処理のたびにバリアをいれる必要があるという点です。
例えば LOD 2 のすべてのメッシュレットを判定するとして、次の LOD 1 で判定すべきメッシュレットは LOD 2 のすべての処理が終わってから実行できます。
ということは、LOD 2 の処理が終わっていることを保証しなければならず、それ故に各レベルごとにバリアが必要になるわけです。
当然ですが、パフォーマンスも悪くなります。
そこでもう1つの方法を実装します。
これは前回の並列処理をGPUで実装するだけです。
つまり、単純にすべてのメッシュレットに対して処理を回すだけです。
実際のコードは traverse_tree.lib.hlsl の 73行目です。
IsMeshletVisible 関数は前回のCPU実装と同じものをGPUで実装しています。
サンプルではTraverse TypeをGPU Computeに設定するとこの手法を利用します。
当然ですが、rwCountBuffer は事前にクリア処理をしています。
こちらは同ファイルの 31行目からの ClearCountCS 関数です。
この関数はここからのWork Graphを利用した手法でも利用しています。
今回の記事の本題となるWork Graphを利用した実装について解説します。
Work Graphを利用する際のAPIやノードの種別などについては以前の記事をご参照ください。
今回は複雑なグラフは作っていないので、解説するコードは以下のシェーダコードのみです。
Work Graphのノードは1つだけのThread Launch Nodeです。
Thread Launch Nodeは複数のスレッドをまとめて実行するのではなく、単体で実行することを目的とするノードです。
そのため、Input Recordはスレッドに対して1対1で設定されます。
やっていることはCPUのリニア処理とほぼ同様です。
Input Recordから入ってきたメッシュレットIDからメッシュレット情報を取得し、IsMeshletVisible 関数で表示か非表示かを決定します。
表示の場合は Compute Shader バージョンと同様にIndirect Draw変数へ書き込みます。
非表示の場合はRecursiveNodeを子メッシュレットの分だけ起動します。
つまり再帰的にメッシュレットの可視性を判定するだけです。
注意点として、Work Graphで再帰を行う場合、再帰の最大深度を設定しておく必要があります。
それが NodeMaxRecursionDepth 修飾子です。
今回は16回に設定していますが、この数はLODレベルの最大数を考慮する必要があります。
サンプルでは最大のレベルが11ですので、12回の再帰ができればOKです。
16を超えるようであれば再帰回数を増やすか、もしくは最大レベルを15で止めるようにしたほうが良いでしょう。
当然ですが、再帰回数が増えれば増えるほどワークバッファのサイズが増えます。とりあえず大きな値にする、というのはやめたほうが良いでしょう。
Work Graphを利用する場合は、Traverse TypeをGPU WorkGraphに変更する必要があります。
今回はLODの選択のみを行っていますが、実際にエンジンに組み込む場合は事前にメッシュ単位でのカリング処理、LODを決定した際についでにメッシュレットカリング処理もいれることになると思います。
CPUではメッシュ単位でのフラスタムカリングは出来ますが、オクルージョンカリングやメッシュレットカリングは難しいので、GPUで実装するならGPUでしか出来ない高速化を行うのが良いでしょう。
アルゴリズムについては前回解説していますし、GPUで実行するにしてもシェーダは非常に簡単です。
解説もほとんどしていないのですが、多分多くの人が気になっているのはWork Graphを利用した場合のパフォーマンスではないでしょうか。
幸いにも Nsight Graphics 2024.2.0 ではWork GraphのGPU Traceが可能です。Frame Debuggerは動作しませんが…
以下は起動時のカメラ位置から見た場合の結果の比較です。
上部がCompute Shader版、下部がWork Graph版です。
CS版は0.01ms未満に対してWG版は0.07msとなりました。
たった1つのメッシュの処理でも0.07msかかるのであれば、単純計算で100個のメッシュで7msかかるということになります。
正直なところ、割に合わないという感じですね。
起動時のカメラ位置ではLOD 1が支配的な状況ですので、それなりに再帰の深さがある状態と考えられます。
そこまで深くなることはあまり多くないとはいえ、それでもあまり現実的とは言えないでしょう。
では、カメラをだいぶ引いて再帰の深さが少ない状態ではどうでしょうか?
CPUバージョンではそのくらいになるとリニアに処理したほうが高速でしたが、GPUはどうなるか。
CS版は当然変化なく0.01ms未満ですが、WG版は0.04msまで高速になりました。
残念なことに、再帰の深さが浅い状態でもWG版は低速でした。
Work Graphはまだ現段階ではNVIDIA GPUについては高速とは言えないようです。
もちろん、1つのメッシュだけでなくもっと多くのメッシュを一度に処理するようにすればもう少し改善するかもしれませんが、CS版とパフォーマンスが逆転するかどうかは不明です。
AMD GPUであればもっと良い結果が得られるかもしれませんが、どうなんでしょうか?
LOD選択をGPUで実装してみましたが、期待しているWork Graphはまだまだ発展途上のようです。
他の方の検証でもWork Graphはパフォーマンスが悪くなっているようなので、期待通りの結果が得られるのはもうちょっと先になりそうです。
今のところは素直にIndirect Draw/Dispatchを使っておきましょう。