DirectXの話 第151回

DirectX Raytracing Fallback事始め その2 18/04/10 up

前回に引き続き、サンプルを見ていきましょう。

サンプルコードはこちらのSample01です。

https://github.com/Monsho/DXRSamples

サンプルコードを見ていく続き

InitGeometry()

この命令ではジオメトリの頂点バッファとインデックスバッファを作成しています。

頂点バッファは今回は頂点座標のみです。

ただの四角形を描画するので、頂点は4つ、インデックスは6つで、それをUpload Bufferに格納しているだけです。

これ自体は普通に頂点バッファやインデックスバッファを作成しているだけなので、これ以上の解説は不要でしょう。

InitAccelerationStructure()

この関数では Accerelation Structure (以下AS) を生成します。

聞きなれない名前ですが、いわゆる BVH 的なものを生成します。

レイトレーシングはレイをシーン中に飛ばして衝突判定をとるわけですが、闇雲にレイとジオメトリの衝突判定をとっていても時間がかかりすぎてまともな速度で動作しません。

そのため、シーンを構築する際にあらかじめ空間分割を行っておいてレイとジオメトリの詳細判定以前に枝刈りを行うわけです。

その際によく使われるのが Bounding Volume Hierarchy (BVH) です。

しかし、BVH 的なと記述したのには理由があり、DXRではこのシーン構築を BVH でやっているとは保証されません。

例えば Octree などが使われているかもしれませんが、詳細は不明です。最終的にはドライバ側の実装によるのかなという気がします。

そのため、BVH とは呼ばずに AS と呼称しているのでしょう。

DXR では AS はボトムとトップという2つのレベルによって構築されます。

ボトムレベルはメッシュの形状を定義するレベルで、トップレベルはそのメッシュがシーンのどこにどのような姿勢で置かれているかを定義するレベルです。

わかりやすい例として、ゲームエンジンでのシーン構築を考えてみましょう。

例えば、シーンに1つのテーブル、2つの椅子が置かれていると考えてみてください。椅子は同じものが2つ置いてあるものとします。

このようなシーンをゲームエンジンで実装する場合、まずテーブルと椅子のメッシュリソースを用意することになります。

DCCツールで作成したこれらのメッシュをエンジンにインポートし、これをシーンに配置します。

シーンにはアクターとしてテーブルと椅子を配置します。椅子はアクターが2つ配置されることになります。

2つの椅子アクターは別々の位置、姿勢で配置されますが、参照しているメッシュリソースは同じものになります。

つまり、アクターによってメッシュがインスタンシングされているわけです。

このメッシュリソースとアクターの関係がほぼそのままボトムレベルとトップレベルに該当します。

実際、トップレベルに登録されるデータは”インスタンス”と呼ばれます。

若干の違いとしては、ボトムレベルASは基本的に1メッシュ1マテリアル構成となります。

これはシェーダテーブルとの関係があるのですが、回避策もなくはないです。

詳しくはシェーダテーブルを含めて後述します。

AS の構築自体は DXR がよしなにやってくれますが、必要な情報やバッファはユーザが作成する必要があります。

Sample01.cppの 966-1002 行目では、AS 構築に必要なバッファサイズを計算しています。

トップレベルASの場合はインスタンス数を指定して計算します。976行目の NumDescs がそれです。今回は1つだけにしています。

ボトムレベルASの場合はジオメトリ情報を指定します。

ジオメトリ情報は 950-962 行目で設定されています。ここには InitGeometry() 命令で作成した頂点バッファとインデックスバッファが設定されます。

トップレベルASはシーン1つにつき1つだけ生成されますが、ボトムレベルASはメッシュリソースの数だけ生成されます。

また、ボトムレベルASもメッシュリソースとは言っていますが、例えば1つのメッシュの中に複数のマテリアルが存在している場合は、そのマテリアルが割り当てられているジオメトリごとに分けて生成する必要があります。

これはシェーダテーブルがトップレベルASのインスタンスごとに割り当てられるからです。

次に 1004 行目からですが、ここではスクラッチリソースというものを生成しています。

このリソースは AS 構築時に必要な一時バッファです。

各レベルASのサイズ計算時に同時にこのスクラッチリソースに必要なサイズも取得できます。

各 AS に必要なスクラッチリソースサイズの最大サイズを確保しておけばほぼ問題ないでしょう。

1016 行目からは各ASのリソースを生成しています。

このリソースはUAVとしてアクセスされるため、D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS フラグを立てておく必要があります。

これはスクラッチリソースも同様です。

1034 行目からはトップレベルASに登録するインスタンスの情報をバッファに格納しています。

ボトムレベルASの構築に必要な頂点バッファとインデックスバッファは既に生成されていましたが、インスタンス情報を格納したバッファは作成されていませんでしたので、ここで作成します。

なお、トップレベルASの作成が完了したら、このインスタンス情報バッファは不要になります。

1065 行目から各ASの生成に必要な記述子を設定し、1088 行目のラムダ式で各ASを構築します。

構築はGPUで行われるらしく、コマンドリストの命令を実行することで構築が完了します。

ここで注意点としては、トップレベルASを構築する前にすべてのボトムレベルASを構築しておかなければなりません。

ボトムレベルASの構築完了を待つため、ボトムレベルASにバリアを張ることを忘れないようにしましょう。

ASの構築はGPUで行われるため、毎フレーム更新することももちろん可能ではあります。

ただ、ボトムレベルASの更新はできるだけ行わないことを推奨します。

トップレベルASの構築は高速ですが、ボトムレベルASの構築はかなり低速です。

InitShaderTable()

さて、初期化処理も最後です。

ここではシェーダテーブルを生成します。

シェーダテーブルの名前は以前の解説中にも何度か出てきていますが、どういうものなのでしょうか?

シーンを構築する際、メッシュの形状だけでなく、その形状にどのようなマテリアルが割り当てられているかが重要になります。

マテリアルの違いはどのように表現するかというと、テクスチャの違いであったり、パラメータの違いであったり、シェーダそのものの違いであったりもします。もちろん、そのすべてかもしれません。

ラスタライザにおけるマテリアルの変更方法はどういうものがあるでしょう?

Forward Renderingの場合、マテリアルの違いはDrawCallごとにテクスチャやシェーダを切り替えて対応します。

Deferred Renderingの場合はちょっと面倒で、シェーダ自体は切り替えず、テクスチャやパラメータだけで基本的に対応します。

シェーダIDのようなものを利用して何でもできる Uber Shader を用いて内部分岐させる方法も一般的です。

レイトレーサーの場合、シーンに飛ばしたレイが衝突したジオメトリに設定されているマテリアルに対して処理を行いたくなります。

DXR ではシェーダ命令の中に InstanceIndex() という、衝突したインスタンスの番号を取得する命令が存在します。

インスタンス数分だけのパラメータやテクスチャの配列が存在すれば、このインスタンス番号からテクスチャ等は持ってこれるでしょう。

また、Uber Shader を用いることで、複数のライティングメソッドを切り替えることも可能です。

しかし、DXR にはそれ以外の手法も提供されています。

それがシェーダテーブルです。

このシェーダテーブルは、シェーダプログラム1つとそれに対して割り当てるシェーダリソースの組みをシェーダレコードと定義し、これを複数個、配列として設定しているものです。

ここで設定されるシェーダリソースは、ローカルルートシグネチャで定義されたリソースの実体となります。

シェーダテーブルは Ray Generation Shader, Miss Shader, Hit Group の3種類を作成することになります。

この中で特に重要なのは Hit Group のシェーダテーブルです。

Hit Group シェーダテーブルは、基本的にマテリアルの数だけ用意されます。

シェーダプログラムとそこで使用されるリソースの組み合わせ、というのはまさにマテリアルそのものと言えます。

では、このマテリアルとなるレコードはジオメトリ側からどのように設定されるかというと、トップレベルASのインスタンスごとに設定します。

D3D12_RAYTRACING_FALLBACK_INSTANCE_DESC::InstanceContributionToHitGroupIndex という変数にHit Groupシェーダテーブルの番号を指定します。

DXR はレイがヒットした際に上記の変数を見て適切なシェーダレコードを取得し、そこに設定されているシェーダプログラムに、やはりレコード内のシェーダリソースをシェーダプログラムに渡します。

プログラムはさほど難しいことはありません。

関数の最初にシェーダプログラムの識別子を取得します。

識別子はパイプラインステートから取得できます。

1157 行目からはシェーダリソースの構造体を作成しています。

この構造体がレコード内のデスクリプター部分として機能します。

サンプルでは RayGenCB という構造体のみですが、実際にこれを利用しているのは Ray Generation Shader だけです。

今回のサンプルでは各シェーダをすべて1つのローカルルートシグネチャとバインドしてしまっているので、このシェーダリソースはすべてのシェーダレコードに設定します。

1168 行目からはシェーダテーブルの生成処理です。

アップロードバッファに必要なシェーダ識別子と、上記で用意しているルート変数を格納するだけです。

注意点としてですが、現在の Fallback Layer ではどうも複数のシェーダプログラムには対応していないんじゃないかと思われます。

対応していないのは複数の”シェーダプログラム”であって、シェーダレコードは複数作ることもできます。

また、これも Fallback Layer 絡みですが、ローカルルートシグネチャに指定できるシェーダリソースは D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS で指定した32ビットの定数バッファのみです。

CBVやSRVは指定できませんが、正式版のDXRでは指定できるようになるはずです。

LetsRaytracing()

初期化が終わったのでレイトレーシングしてみましょう。

レイトレーシングするには D3D12_DISPATCH_RAYS_DESC (D3D12_FALLBACK_DISPATCH_RAYS_DESC) という記述子に必要な情報を設定し、DispatchRays() メソッドに PSO とこの記述子を渡すだけです。

この記述子に設定するのはシェーダテーブルとレイを飛ばすスクリーンの幅と高さです。

シェーダテーブルは各シェーダテーブルの先頭アドレスとシェーダテーブルバッファのサイズ、およびシェーダレコードのサイズを指定します。

ただ、Ray Generation Shaderはストライドを必要としません。

なぜなら、Ray Generation Shaderは1回の DispacthRays に対して1つしか起動できないためです。

設定している Ray Generation Shader のアドレスをシェーダレコードとして取得して実行します。

この命令が終了すると、ターゲットのUAVにレイトレーシングの結果が入っています。

これをSwapchainにコピーすれば結果を表示することができるでしょう。

ちなみに、Sample01 の実行結果は以下のようになります。

という感じで、簡単ですがサンプルコードの解説を終わります。

まだよくわからない部分も多いので、いろいろサンプルコードを作りながら調べてみたいと思います。

少し長い目で見ていただければ。