DirectXの話 第175回
Bottom Level ASの更新
21/02/14 up
今回のサンプルはBottom Level Acceleration Structure (BLAS) の更新についてです。
GitHubのいつものページですが、前回のサンプルである Sample026 に機能を追加している点に注意してください。
追加しているのはBLASの更新と、それに必要なGPUでの頂点変換処理です。
これに伴ってメニュー内に "BLAS update flag" というチェックが増えています。
このチェックはBLASを更新するかどうかを指定するフラグではないので、OFFにしてもスザンヌさんが秘孔を突かれたように歪むのは止められません。ごめんよ、スザンヌさん…
Acceleration Structure を更新するということ
ゲームアプリケーションに限った話ではありませんが、ほとんどのCG映像は画面の情報が更新されていきます。
カメラが動くだけであれば毎フレームのレンダリング結果が変化するだけの話ですが、多くの場合、オブジェクトが動きます。
それも位置が移動する、回転するというレベルではなく、人間や動物、メカなどは骨の動きに合わせて動作することになります。
さて、DirectX Ray Tracing (DXR) ではシーン内のオブジェクトを Acceleration Structure (AS) という形で表現します。
これは以前にも解説しましたが、Top Level と Bottom Level の2層から成り立っています。
Top Level Acceleration Structure (TLAS) はシーン内のオブジェクトの位置や回転、スケールなどを定義し、そこから空間分割構造を作成しています。
たとえボーンによる変形を行うオブジェクトがなかったとしても、ゲームの場合はほぼ毎フレーム更新されるものです。
対して BLAS は変化するものが限定されます。
サンプルで言えば Sponza メッシュは布が揺れ動いたりしないので完全に静的なメッシュです。
他にもゲーム中に出てくるテーブルや椅子のような家具類など、かなり多くのものが破壊可能でない限り動作しません。
しかし、ボーンで動く生物などのオブジェクトや、木や草のように風で揺れるオブジェクトは頂点座標がいろんな要因で変化します。
通常のラスタライザであればそのオブジェクトを描画するタイミングで頂点シェーダで変化させればいいのですが、レイトレーシングではレイを飛ばすタイミングでオブジェクトの変化は完了していて、その上 AS として登録されていなければならないわけです。
現在、DXR では AS の更新が GPU で行われます。CPU での更新には対応していません。
そして AS 更新には頂点バッファとインデックスバッファが必要で、これらは更新タイミングで座標などが確定されていなければならないわけです。
ラスタライザでは描画タイミングで頂点シェーダで行われる変換が、AS 更新では事前に行われていなければならないということになります。
頂点キャッシュ
そこで重要になるのが頂点キャッシュです。
まあ、この名前はエンジンによっても変わってきます。UE4だとスキンキャッシュだし。
これはつまり、通常であれば頂点シェーダで変換されてあとに残らない頂点変換後のデータをキャッシュとして残しておくという考え方です。
今回はシーン中に配置されている2体のスザンヌさんのうち1つに頂点キャッシュを適用しています。
ボーンが入っているわけではないので、UE4でいうところの World Position Offset での変換を行っています。
この変換には Compute Shader を用いていますが、ボーンによるデフォームでも同様に CS を利用するのが一般的です。
Geometory Shader 後に Stream Output という頂点変換結果を出力する機能もあったりするのですが、多分これを使って頂点キャッシュを行う人は稀でしょう。
ちなみに、私は今の今まで使ったことがありませんし、これからも使う予定はありません。
頂点キャッシュは1つのメッシュを複数回描画するような場合で有利になる可能性があります。
ただ、経験上あまりそのような場面は多くないです。
GPUが弱くて頂点シェーダネックになりやすいハードでも頂点キャッシュによる速度的なメリットは特にありませんでした。遅くなる場面はありましたが。
その上、メッシュデータとしての頂点バッファとは別にキャッシュバッファを用意しなければならない、しかもインスタンスごとというデメリットがあります。
CS で頂点変換を行う関係で、いわゆる Input Layout が利用できないという問題もあります。
頂点バッファフォーマットがF16とかU8の場合はそれに合わせた Load/Store を行わなければなりません。
ただ、ボーンの影響数に応じた CS を用意しておけば頂点シェーダのバリエーションが大きく減るという利点もあります。
CS なので非同期コンピュートを使用することもできなくはないです。
ただ、そのタイミングでグラフィクスパイプで動作させる処理が結構限定されます。頂点キャッシュの計算が完了する前に実行できる処理を考えないとならないので。
このようにメリットはあるもののデメリットもある頂点キャッシュですが、DXRを使う場合はほぼ必須案件となります。
まあ、頂点キャッシュを使うべきか使わないべきかで迷う必要がなくなる、というメリット?はありますね。
サンプルでは vertex_mutation.c.hlsl が頂点キャッシュ生成のシェーダコードです。
Y軸方向にサインカーブでグニョグニョ動くだけで、頂点ノーマルも変換していないので特に難しいことはしていません。
ただ、頂点キャッシュが生成されることが確定しているので、ラスタライザでの頂点シェーダは何も変更していません。
AS を更新する
AS の更新方法は主に2つの方法があります。
1つは現在の AS を破棄して新しく作り直す方法です。
AS バッファ自体を破棄すべきかどうかは作り直した場合の AS バッファが元のバッファサイズより大きいかどうかで決まります。
元のバッファサイズより大きい場合は作り直しが必要ですが、同一サイズや小さい場合はバッファの作り直し自体は不要です。
これはスクラッチバッファも同様です。
しかし単純に作り直しなので、AS ビルドを普通にGPUに投げてやればOKです。特に難しいことはありません。
もう1つの方法は AS ビルド時に更新を許可するフラグを立てる方法です。
新規に AS をビルドする際に D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE フラグをビルドフラグに追加します。
このフラグを追加した場合の新規ビルドは通常のビルドより速度面で劣り、また、メモリも多く使用するようです。
ただし実際にどの程度多くメモリが消費されるかはGPUドライバによるものと思われるので、GPUメーカーの実装次第では特に重くなったりメモリ量が増えたりはしないかもしれません。
実際に更新する際には構築時に使用したフラグに加えて D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE フラグを立てます。
新規ビルドのときとは違い、こちらのビルドは通常の AS ビルドより高速になります。
ただしアップデート時には TLAS の場合はオブジェクト数、BLAS の場合はポリゴン数が増減してはいけません。
また、DXRの仕様では、AS 構築時に指定する行列は NULL でも構いませんが(この場合は単位行列になる)、更新フラグを利用する場合は NULL か非 NULL かは固定されなければなりません。
新規ビルド時に NULL だったら更新時も NULL、新規ビルド時に非 NULL だったら更新時も非 NULL でなければならないというわけです。
更新された AS はレイトレーシング時の速度にペナルティが発生する可能性があります。
これは更新処理の内部実装によるとは思いますが、多分内部的にはBVHのツリー構造を更新していないのではないかと思います。
つまり更新されるのは頂点座標から求められるバウンディングボリュームだけなのではないかと。
これはあくまでも予想なので絶対にそうだとは言えませんが、そのような実装であれば大きな形状変化は明らかにレイトレーシングのパフォーマンスに影響を与えるでしょう。
これらを踏まえると、TLAS は作り直し、BLAS は更新フラグでの実装が望ましいと思います。
ゲームアプリケーションでは特にオブジェクトの表示/非表示は頻繁に行われます。
敵が死んだらオブジェクトは減りますし、敵がポップすれば増えます。
しかもキャラクターはシーンを大きく移動することが多いので、BVHのツリー構造が更新されないと不利になりやすいです。
逆に BLAS は頂点の増減が基本的にありません。
その上、TLAS のビルドに比べると基本的に負荷が高いです。
とはいえ、ボーンで毎フレーム変化するからと言って毎フレーム更新するべきかは疑問です。
特に遠距離に存在するキャラクターなどは更新を数フレームに1回とかでもいいかもしれません。
今回のサンプルでは TLAS はバッファの作り直しも含めた AS の作り直しを行い、BLAS はユーザー指定で更新フラグか AS の作り直しを行っています。
メニューにある "BLAS update flag" がONの場合は更新フラグを利用し、OFFの場合は AS を作り直します。
BLAS と TLAS の更新を合わせた速度もメニュー内に表示されているので、更新フラグのON/OFFでどの程度差がでるか確認できると思います。
なお、私の環境(RTX2080)ではON/OFFで0.2msほどの差が出ていました。
これは小さいと思う方もいらっしゃるかもしれませんが、スザンヌ1体の BLAS 更新方法が違うだけでこの差がでると考えると、実際のアプリケーションでは結構な差がでると思われます。
やはり可能な限り更新フラグを使ったほうが良いでしょう。
非同期コンピュートによる更新
前回のサンプルでは AS 更新はグラフィクスパイプで行っていましたが、今回はサンプルを改造して非同期コンピュートで更新するようにしています。
ただ、頂点キャッシュはグラフィクスパイプで行っています。これは頂点キャッシュ計算後に Z Pre Pass などで使用しなければならないためです。
頂点キャッシュを使うものと使わないもので描画を分けていれば、頂点キャッシュを使わないものをグラフィクスパイプで描画している間に頂点キャッシュ計算を行う、なんてこともできるとは思いますが、実際にそういう描画パスを作ろうとすると結構面倒ですよね。
なお、UE4も同様の処理になっています。
まあ、現在は非同期コンピュートによる AS ビルドはバグってて動作しませんが。
非同期コンピュートがきちんと動作しているかは Nsight Graphics なんかを使うとわかりやすいです。
基本的に AS ビルドは非同期コンピュートに回してしまうほうがいいと思います。
というわけで今回は終了です。
次に何をやるかは決めてません。