DirectXの話 第150回

DirectX Raytracing Fallback事始め その1 18/04/08 up

話題のDirectX Raytracingをいじってみたのでとりあえず現段階のまとめとして記事を書くことにします。

知っての通り、DirectX Raytracing (以下DXR) はDirectX12と統合されたGPUレイトレーサーです。

DirectX12と統合されているため、レイトレーサーとラスタライザーの同時利用がしやすく、各種リソースへも一元的にアクセスできるという利点があります。

GPUレイトレーサーは他にもあるのですが、この統合されているという点が強みということで注目されています。

DXRはまだまだ開発途中のもので、APIなどに変更がある可能性が高いです。

また、実際にはDXRは使用できず、Fallback LayerというMSが開発した、Compute Shaderを使ったエミュレータのようなものを使用する必要があります。

Fallback LayerはMSがGitHubにコミットしています。

https://github.com/Microsoft/DirectX-Graphics-Samples

こちらに今回の記事の元ネタのサンプルもあるので、そちらをチェックしてみてもいいでしょう。

なお、Fallback Layerはコードも公開されているので、やろうと思えば Vulkan でも実装できるかもしれませんね。

また、MSのサンプルよりNVIDIAのチュートリアルの方がステップバイステップで解説している分わかりやすいです。

ただし、こちらはFallback Layerに対応していないため、現在動作させることはできないでしょう。

@shikihuiku さんがわかりやすい解説も行ってくれているので、この記事を読むより有用じゃないかと思います。

https://shikihuiku.wordpress.com/2018/04/04/nvidiaのdirectx-raytracing-tutorialsを見てみる/

今回のサンプルについて

今回作成したサンプルは上述したMSのサンプルから必要なものを取り出した簡易版という感じのものです。

1つの.cppファイルにシェーダコード以外の必要なコードを集約していますので、処理の流れは追いやすいのではないかと思います。

サンプルはこちら。

https://github.com/Monsho/DXRSamples

すでにSample02まで存在しますが、今回はSample01の方を使用します。

実行方法

まずMSのサンプルの実行を目指しましょう。環境さえ整っていればさほど難しくないはずです。

実行できたら DXRSamples.sln の上層フォルダに、MSのサンプルから以下のフォルダをコピーします。

DirectX-Graphics-Samples/Libraries

DirectX-Graphics-Samples/Samples/Desktop/D3D12Raytracing/tools

これで DXRSamples.sln を立ち上げれば Fallback Layer のプロジェクトも含まれているはずです。

もし含まれていないなら "DirectX-Graphics-Samples/Libraries/D3D12RaytracingFallback/src/FallbackLayer.vcxproj" をソリューションに追加しましょう。

これでまあ、たぶんビルドできるはずです。そして、ビルドできればそのまま実行も行けるはず。

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

では、順番にサンプルコードを見ていきます。ソースコードは Sample01.cpp の 1328行目にある WinMain() の処理の順番通りに見ていきましょう。

InitWindow()

普通にウィンドウを作成するだけです。

InitDevice()

D3D12の基本的なオブジェクトを生成しています。

デバイス、スワップチェイン、キューなどですね。

D3D12の基本的な部分の解説は行いませんが、この関数で1点だけ重要な部分があります。

関数の最初に EnableRaytracing() という関数を呼び出し、失敗したら EnableComputeRaytracingFallback() という関数を呼び出しています。

この2つの関数は 145行目から書かれているものですが、D3D12の実験的フィーチャーを有効にする EnableD3D12ExperimentalFeatures() を適切に呼び出すようにしています。

現段階では正式なDXRは使用できないため、EnableRaytrancing() は必ず失敗します。

私はこの命令の呼び出しをしていないという単純なバグで数時間ふっとばしました。

InitRaytraceDevice()

さて、ここからが本番のコードになっていきます。

一応、MSのサンプルをまねてDXRでのコードも書いていますが、動作までは保証できません。

なので、ここからの解説は Fallback Layer での解説のみを行っていきます。

この関数ではDXRで使用するデバイスとコマンドリストを作成します。

正式なDXRでは QueryInterface を実行するだけですが、Fallback Layer ではちょっとやり方が変わります。

デバイスは D3D12CreateRaytracingFallbackDevice() を用いて作成し、コマンドリストはその生成したデバイスの QueryRaytracingCommandList() メソッドを用いて変換します。

コマンドリストはトリプルバッファにしているので、すべてのコマンドリストについて QueryInterface します。

ここはお約束の初期化という程度なので詳しい解説は行いません。

test.r.hlsl

ここでいったんシェーダコードの解説に移ります。

ここから先でパイプラインステートを生成しなければならないのですが、そのためにはコンパイル済みのシェーダコードが必要になるからです。

このファイルには3つのエントリーポイントが存在しています。それぞれ、[shader("~")] と先頭に書かれている命令がそれです。

RayGenerator()

raygeneration とタグ打ちされている関数です。名前の通り、レイの生成を行います。

この関数はレイの生成を行い、レイをトレースし、その結果を出力する、レイトレーサーの真のエントリーポイントと言える命令です。

レイを生成するにはまず自身を起動したピクセルがどこなのかを知る必要があります。

float2 lerpValues = (float2)DispatchRaysIndex() / DispatchRaysDimensions(); float3 rayDir = float3(0, 0, 1); float3 origin = float3( lerp(cbRayGen.viewport.left, cbRayGen.viewport.right, lerpValues.x), lerp(cbRayGen.viewport.top, cbRayGen.viewport.bottom, lerpValues.y), 0.0f);

DispatchRaysIndex() 命令はこの関数を実行しているピクセル座標を取得します。

DispatchRaysDimensions() 命令は画面全体の幅と高さを取得します。

このサンプルではとりあえず正射影ということにして、そのままスクリーン奥行き方向にレイを飛ばします。

RayDesc myRay = { origin, 0.0f, rayDir, 10000.0f }; HitData payload = { float4(0, 0, 0, 0) }; TraceRay(Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 0, 1, 0, myRay, payload);

実際にレイを飛ばしているコードはここです。

RayDesc構造体はレイの開始位置、レイを飛ばす開始深度、レイの方向、レイの最大長を指定します。

こちらはDXRで定義されている構造体です。レイを飛ばす場合は必ずこの構造体に情報を格納しなければいけません。

HitData構造体はユーザが定義できる構造体です。レイトレースの結果を受け取るための構造体です。

今回はカラーのみを受け取るようにしていますが、例えばヒットしたマテリアルの番号やジオメトリの番号なども取得できるようです。

例えばシャドウ計算に使うなら遮蔽されているかどうか、AIに使うならレイがヒットした相手が敵か味方かなどの取得も可能なわけです。

TraceRay() 命令はレイトレースの肝となる関数です。この命令を発行しなければレイは飛びません。

第1引数はシーンを構築している Acceleration Structure (AS) を指定します。ASについては後述しますので、現段階ではシーンそのものと思ってもらえれば結構です。

Scene は予約語ではなく、HLSLコードの上部で RaytracingAccelerationStructure 型で定義されている ShaderResourceView です。

第2引数はフラグです。どのようなフラグがあるかはドキュメントを参照してください。

第3引数はレイの判定を行うマスクです。通常は 0xff を指定します。

マスクはシーンのインスタンスごとに設定可能で、このマスクによって接触判定をとらないインスタンスを指定することができます。

第4, 5引数は接触したジオメトリに対して起動するシェーダテーブルのインデックスを算出するためのオフセット的な値です。

詳しくはドキュメントを読んでいただいた方がいいかと思いますが、通常は 0, 1 を指定すればOKです。

第6引数は Miss Shader のインデックスです。

Miss Shader はレイがどこにも当たらなかった場合に起動するシェーダです。

第7引数は飛ばすレイの情報、第8引数は結果を取得する Payload です。

ClosestHitProcessor()

closesthit とタグ打ちされている関数です。この関数はレイが接触した場合に起動します。

関数の引数としては Payload (入出力に使用する構造体) と Intersection Attributes (接触判定の結果の情報) を取ります。

Payload は TraceRay() 命令に渡したものがそのまま来ます。

出力したい情報があればこの構造体に情報を乗せればOKです。

Intersection Attributes は今回は barycentrics、つまり接触した三角形の重心を取得できます。

ここから三角形の必要な情報(法線やUV)を求められます。

この構造体はユーザが指定することも可能ですが、その場合は Intersection Shader を自前で実装しなければなりません。

三角形の場合は組み込みの Intersection Shader が利用できますので、今回はそれを使っています。

今回はこの命令で重心座標を色に変換して Payload に渡しています。

MissProcessor()

miss とタグ打ちされているこの関数は、レイがどこにも接触しなかった場合に実行されます。

とりあえず黒を出力するようにしていますが、レイ生成関数内で Payload を黒で初期化しておけばそれでもOKだったりします。

通常であれば天球に使用しているIBLから色を引っ張ってくるなどの処理が行われるのではないかと思います。

InitRaytracePipeline()

さて、C++コードに戻ってきました。この関数ではパイプラインステート (PSO) を生成します。

PSO はD3D12の通常の描画でも使用しますが、レイトレース用のPSO生成方法はD3D12のそれとは結構異なっているので注意してください。

最初にルートシグネチャを作成しています。

パッと見は普通に作っているだけなのですが、通常の描画ではPSO1つにつき1つだけのルートシグネチャが、ここでは2つ作成しています。

GlobalRootSignature はすべてのシェーダで使用されるルートシグネチャです。

つまり、前述した Ray Generation Shader, Closest Hit Shader, Miss Shader すべてでこのルートシグネチャに登録した情報を参照します。

ここに登録するものはどのシェーダから見ても不変な情報、主にシーンに関する情報を設定します。

サンプルでは UAV を1つと SRV を1つ登録しています。

UAVは出力バッファ、SRVは Acceleration Structure です。

LocalRootSignature はシェーダごとに設定可能なルートシグネチャです。

今回は1つだけ生成していますが、複数生成することも可能です。

ただし、基本的に同種のシェーダについては同じルートシグネチャを使用することになるようです。

今回はViewportの情報を渡すのに使用していますが、Viewport情報はグローバルの方が本来は正しいでしょう。

675行目からはPSO記述子を設定するのですが、この設定方法がD3D12のそれとはだいぶ異なります。

D3D12ではPSO記述子が1つあり、ここに設定する情報は頂点レイアウトのような一部を除いて固定でした。

DXRのPSO記述子はサブオブジェクトという可変長の構造体配列を渡すことになります。

このサブオブジェクトにさまざまな情報を乗せる必要があるわけです。

最初に設定しているのは685行目からのDXILライブラリです。

シェーダコードのコンパイルですが、ターゲットプロファイルに lib というものを指定しています。

これは複数のシェーダエントリポイントをまとめたもののようで、DXRではこちらの方がよさそうです。

ラスタライザの描画パイプラインと違ってシェーダの個数が可変なので、このような方法をとることになるのだろうと思われます。

ここでは、ライブラリとしてコンパイルされたシェーダバイナリと、そこに含まれる複数のシェーダエントリポイントを指定します。

701行目から設定しているのはヒットグループです。

DXRには複数種類のシェーダが存在していますが、そのなかでも Intersection, Closest Hit, Any Hit の3種類のシェーダは1セットで用いられます。

この1セットをヒットグループとしてまとめる必要があり、まとめた際にグループ名を設定して、シェーダテーブルで参照します。

複数設定も可能なはずですが、実際にやってみたら1つしか設定できませんでした。Fallback Layerの制限かもしれません。

シェーダテーブルについては後述します。

709行目からはシェーダコンフィグの設定です。

これは Payload と Intersection Attributes のサイズを指定しています。

これは1つのパイプラインで1つだけ設定できますので、つまり1つのパイプラインで実行するシェーダはここで設定したサイズの Payload と Intersection Attributes を使わなければなりません。

716行目はローカルルートシグネチャの設定です。

その下、719行目はこのローカルルートシグネチャとシェーダ、およびヒットグループのバインドを行うサブオブジェクトです。

どのシェーダ、ヒットグループがどのローカルルートシグネチャとバインドするかは配列のアドレスで指定することになるので、std::vector のようなバッファを確保し直すコンテナを使用する場合は要注意です。

この2種類のサブオブジェクトも複数設定することが可能です。

732行目はグローバルルートシグネチャです。これは1つだけ設定が許されます。

735行目からはレイトレースコンフィグです。

レイトレースの深さの回数をここで指定します。

この値は深さであって、同じ深度であれば複数回のレイとレースが可能なのではないかと思います。

そのうち試します。

ここまでのサブオブジェクトを設定したらやっとPSOの生成です。

ここまでの情報に不備があると、おおむねここで失敗します。

関数の最後でUAVを作成していますが、こちらは出力バッファです。

今回はここまで

文字ばっかりが長くなってきたので今回はここまで。

次回でレイトレして画面を出すまで行きたいですね。