DirectXの話 第153回

DirectX Raytracing Fallbackで3Dをやる その2 18/06/09 up

前回からの続きです。

サンプルはこちらのSample02です。

https://github.com/Monsho/DXRSamples

ソースコード解説続き

Bottom Acceleration Structureを作成する

事始めの記事で書いた通り、Acceleration Structureはシーンを構築するデータです。

これにはボトムとトップの2種類があり、ボトムASがいわゆるモデルデータの実体、トップASがモデルデータを参照するインスタンス情報です。

トップASはシェーダ側に渡す必要がある情報で、基本的には1つだけ作成しますが、ボトムASはトップASから参照されるためか複数作成することが可能です。

多分、トップASが参照リンク的な情報を保持しているのでしょう。

さて、複数のポリゴンモデルを定義するにはもちろん複数の頂点バッファとインデックスバッファが必要です。

普通に考えればそれぞれのモデルデータで別々の頂点バッファとインデックスバッファを生成し、ボトムASを作成すべきです。

しかし、今回は諸事情のため、複数のモデルデータの頂点とインデックスを1つの頂点バッファと1つのインデックスバッファにまとめています。

DXRではレイがヒットした位置(正確にはヒットするまでにレイが進んだ距離)、レイがヒットしたポリゴンのプリミティブ番号、ヒットした重心座標を取得することができます。

しかし、ヒットしたポリゴンを形成するインデックス番号、および各頂点の情報(座標だけでなく法線やUVも)はDXRの命令からは取得できません。

そこで、頂点バッファとインデックスバッファをシェーダに送り、プリミティブ番号から対応するインデックス番号を取得し、そこからさらに各頂点の座標を取得し、それらと重心情報からヒット位置の法線やUVを計算します。

つまり、頂点バッファとインデックスバッファをシェーダに送る必要があるのですが、今回はそれをグローバルルートシグネチャによって送っています。

ローカルルートシグネチャを使えばいいのではないか?と思われるかもしれませんが、それを試したところなぜかクラッシュしてしまったので今回は諦めてグローバルルートシグネチャにしています。

その結果、頂点バッファとインデックスバッファを1つにまとめ、ローカルルートシグネチャで送る定数バッファに先頭の番号を渡すことで対処しています。

イメージ的にはこんな感じ。

定数バッファはマテリアルごとに設定することになりますが、今回は1モデルにつき1マテリアルなので、モデルのインスタンス数分だけ定数バッファが存在しています。

そしてそのマテリアルが参照するモデルの頂点バッファとインデックスバッファのオフセットを内部に保持します。

struct InstanceCB { DirectX::XMFLOAT4 quatRot; DirectX::XMFLOAT4 matColor; UINT32 voffset; UINT32 ioffset;};

各モデルのインスタンス情報はこのようになっています。

voffset, ioffset がそれぞれ頂点バッファのオフセットとインデックスバッファのオフセットです。

matColor はマテリアルカラーで、quatRot がインスタンスの回転クオータニオンです。

何故回転情報が必要なのかというと、頂点バッファから取得できる頂点はローカル座標だからです。

頂点データがローカルであるということは、ワールド空間ではヒットした位置の法線が回転してるかもしれないということになります。

なので回転情報が必要なのですが、3x3のレジスタを失うより、float4が1つで対応できるクオータニオンの方がいいだろうという判断です。

詳しい計算方法はシェーダをご覧ください。

ソースコードはまず 954 行目の InitGeometry() 関数で頂点バッファとインデックスバッファを生成しています。

このバッファにはボックスと球の頂点とインデックスがそれぞれ納められます。

次に 1067 行目からの InitAccelerationStructure() 関数でボトムASとトップASを作成しています。

ボトムAS作成時には、ボトムAS用のバッファサイズを GetRaytracingAccelerationStructurePrebuildInfo() 命令で取得します。

この際に設定する D3D12_GET_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO_DESC 構造体には複数のジオメトリ情報を渡すことができます。

これを使えば複数のボトムASを作らずにも1つで問題ないのでは?と思われるかもしれませんが、罠です。

以下はトップASとボトムASの関係を図示したものです。

トップASには複数のインスタンスがあり、それぞれが1つのボトムASを参照します。もちろん、1つのボトムASを複数のインスタンスが参照することもあります。

モデルの姿勢行列はインスタンス1つにつき1つなので、インスタンスが参照するボトムASが持つすべてのサブメッシュに1つの姿勢行列が適用されます。

今回はボックスと球をそれぞれ別々の姿勢にしたいわけですので、ボトムASを分けてそれぞれを参照する別々のインスタンスを作成しなければならないというわけです。

やろうと思えば1つのボトムASにまとめておいてインスタンスを2つ作成してそのボトムASを参照、シェーダ側で InstanceIndex() を取得して対応するという方法もなくはないのですが、かなり面倒なのでやめた方がよいでしょう。

トップASに登録するインスタンスデータは 1183 行目からです。

それぞれのインスタンスが参照するシェーダレコードInstanceContributionToHitGroupIndex パラメータに指定しています。

このパラメータがシェーダレコードの番号を示しているのですが、正確には違います。詳しくは後述します。

インスタンスの情報にある InstanceID パラメータは、シェーダコード内で InstanceID() 命令によって取得できるパラメータです。

InstanceIndex() 命令はヒットしたインスタンスの全体のインデックス番号ですが、それとは別にインスタンスに情報を持たせたいときに使用します。

今回は不要です。

シェーダテーブルの話

事始めでも書きましたが、シェーダテーブルというのはシェーダレコードの配列となっているテーブルデータです。

シェーダレコードは参照するべきシェーダ命令と、そこに提供されるローカルルートシグネチャで定義されたシェーダリソース情報です。

シェーダレコードは基本的にマテリアル数分だけ作成するもの、と以前は書きましたが、実際には少し違います。

それを理解するには、TraceRay() 命令によって飛ばしたレイがインスタンスにヒットした際、どのシェーダレコードが選択されるかというところを理解する必要があります。

ドキュメントにはその選択ルールを以下の計算で行うと書かれています。

それぞれの変数はどこでどのように指定されるのかきちんと理解する必要があります。

  • RayContributionToHitGroupIndex

    • このパラメータは TraceRay() 命令の第4引数で指定するパラメータです。

    • このパラメータを使用する場面としては、例えばシェーディング用とシャドウ用の closest hit shader を分ける場合などです。

    • シェーダテーブルの先頭n個がシェーディング用、次のn個はシャドウ用と分けておけば、シャドウ用シェーダに投げたい場合に n を指定すればよいということになります。

  • MultiplierForGeometryContributionToHitGroupIndex

    • このパラメータも TraceRay() 命令で指定します。第5引数で、通常は 1 を指定します。

    • このパラメータの使い道は…なんでしょうね?

    • いや、何かに使えそうだなとは思っているんですが、どういうときに使えばいいのかまだ見えてません。

  • GeometryContributionToHitGroupIndex

    • このパラメータだけ、ユーザが指定することはできず、DXRのシステムが自動的に割り振ります。

    • このパラメータは名前の通り、ボトムASのサブメッシュ番号です。

    • 例えば上の図でボトムAS0のサブメッシュBにレイがヒットした場合、この値は 1 になります。

  • InstanceContributionToHitGroupIndex

    • これがトップASに登録するインスタンス情報に格納するパラメータです。

    • このパラメータは、このインスタンスが参照するボトムASのマテリアル先頭のインデックス番号を指定します。

    • 上の図でインスタンスB/Cによって参照されているボトムAS1がマテリアル含めて同一のものであると仮定しましょう。

    • このシーンにおいてはシェーダレコードはサブメッシュの個数だけ必要です。つまり、5つです。

    • このうち先頭の3つがボトムAS0のレコード、残り2つがボトムAS1のレコードです。

    • インスタンスB/CはボトムAS1を参照するので、このパラメータはボトムAS1用のレコードの先頭アドレス、つまり3を指定する必要が出てくるわけです。

この図は最も簡単なシェーダテーブルの例でしかありません。

もしも1つのボトムASに対して、インスタンスによってマテリアルが変化するような場合にはそのボトムAS分のレコードを余分に保持し、インスタンスの InstanceContributionToHitGrroupIndex にそのオフセットを設定すればOKです。

この辺りは割とどうとでもなる部分でもありますので、自分なりのテーブルの作り方をしてみてもいいのではないでしょうか?

カメラを回転させる

グローバルルートシグネチャに設定するシーン用の定数バッファを描画前に Map() し、情報を書き換え、Unmap() するだけです。

この辺は普通のラスタライザと同じです。

終了!

というわけで今回の解説は終了です。

複数のHitGroupを使用する方法はさほど難しくはないと思います。

しかし、ボトムASとインスタンス、シェーダテーブルの関係は少し面倒ですね。

今回のサンプルではボトムASが複数あるとはいえ、インスタンスはボトムASと同数ですし、ボトムASもサブメッシュは1つだけでしたので、インデックス計算は簡単でした。

これが複雑なシーンになると、ボトムASには複数のサブメッシュが存在しますし、複数のインスタンスが1つのボトムASを参照することもあるでしょうし、その他もろもろあるかと思います。

現在のサンプルはジオメトリ生成→AS生成→シェーダテーブル生成と順番にやっていってますが、ジオメトリ生成の段階でシェーダテーブルの最低数は求めておく必要がありそうですし、その上でインスタンス次第で増やしたりする必要もあります。

ちゃんとシステム化するときに頭を抱えそうな印象ですね。