この記事は、夏のブログリレー32日目の記事です。
はじめに
最近DirectX Raytracingを触ってみたので、それについて書こうと思います。私自身触ったばかりなので、パフォーマンスが悪かったりよくない書き方をしていたりすると思いますが、とりあえずプログラムを動かすところまでをやりたいと思います。
この記事では、MicrosoftのDirectX-Graphics-SamplesのD3D12RaytracingProcedualGeometryサンプル(サムネ画像)を理解して実装できるようになることを目標にしようと思います。
...と思っていたのですが、想定していたよりも大変だったのでとりあえず三角形を表示するだけをやろうと思います。もし機会があれば続きを出すかもしれないです。個人的にこのサンプルはとても「レイトレーシングっぽい」と思っているので、目標にしたいですね。
リポジトリは↓です。ビルドシステムはCMake(+ vcpkg)です。章ごとにタグで分けているので参考にしてみてください。
DirectX Raytracingに入門するにあたって、NVIDIAのチュートリアル[1]が非常に参考になりました。ぜひ見てみてください。
また、MicrosoftのDirectX Specs[2]が公式仕様です。こちらも確認してください。
レイトレーシングとは
レイトレーシングは、3D空間から描画する2D画面を計算する手法の一つです。レイトレーシングと聞くと最近出てきたリアルな画像を生成する技術という印象がありますが、実は手法自体はかなり古くから存在しています。初めてレイトレーシングの考え方が発表されたのは1968年の論文[3]ではないかといわれています。C言語が登場したのが1972年なので、それよりも前です。
仕組み
現実世界では、太陽などの光源から光が出て、物体に当たり、それが反射して目に届くことによって物体を認識します(図左)。レイトレーシングではこの逆をたどります。視点から、3D空間上に考えたスクリーンのピクセルをめがけて光線を射出します。そして、その光線が何か物体に当たれば、「このピクセルにはその物体を描画すればよい」と考えます。また、光線が物体に当たった後、反射や屈折などを計算し光源に当たったのならば、物体には光源からの光が届いていると考えることができるので、物体の表面に光の映り込みなどを描画します。
もう少し数学を用いて考えてみます。
視点を 、目標のスクリーン上のピクセルを とすると、パラメータ を用いて光線の方程式は
となります。これと、例えば点 を中心とする半径 の球との交点を考えます。球の方程式は
です。これら2式を連立させると の2次式になるので、光線と球との交点での の値が求められます。解がなければ光線は交わらず、2つあれば が小さいほう(=視点に近いほう)を交点として採用します。
特徴
先ほど説明したように、レイトレーシングの仕組み自体はとても単純で、高校数学の範囲で十分説明できるほどです。レイトレーシングの特徴として、現実世界の光の動きをシミュレートするためとてもリアルな画像を生成できるという点があります。特に、「影」「AO(環境光遮蔽)」などは従来のラスタライズ法と比べて簡単かつ高品質に描画することができます。
しかし、現在までレイトレーシングは(特にゲームなどの速度が重要なものにおいて)あまり使われてきませんでした。理由は簡単で、とても重いからです。レイトレーシングでは光線と物体との衝突判定によって描画するものを決めます。衝突判定する物体は基本的に3D空間内の全物体(もっと言えば、全オブジェクトのすべての三角形)なので、計算量が膨大になります。「Ray Tracing in One Weekend[4]」などをやってみた方ならわかると思いますが、CPUでレンダリングしようとすると300 x 500くらいの画像でもレンダリングに1枚80秒くらいかかってしまいます。
しかし、GPUの進化によってレイトレーシングに最適化されたコアが搭載され、リアルタイムにレイトレーシングでレンダリングができるようになってきました。
DirectX Raytracing
レイトレーシングは各ピクセルごとに独立した処理を行うため、GPGPUと同じように処理をすることができます。DirectX11の時には、ComputeShaderを用いてレイトレーシングを実装しているものもあったそうです。しかし、それではシェーダー内で再帰が使えないなどの制約があり、十分に表現することはできませんでした。そんな中、2018年にDirectX12の機能としてDirectX Raytracing (DXR) が登場し、APIレベルでレイトレーシングがサポートされるようになりました。
画面を表示する
では、DXRを触ります。その前にまずはDirectXの初期化です。DXRでないDirectXと同じように、デバイス、スワップチェーン、コマンドなどの設定をします。
それらが終わったコードはタグ01_init
にあります。三角形の頂点バッファも作っておきました。本当なら頂点バッファをDEFAULTヒープにしたり、D3D12MemoryAllocatorを使ったりした方がよいのですが、簡単のためにここでは省略します。
以下、見出しにある括弧の中身がgitのタグの名前です。
サポートのチェック(02_check_availability)
DXRの機能に入る前に、ハードウェアがレイトレーシングに対応しているかを確認する必要があります。CheckFeatureSupport
メソッドで確認できます。
D3D12_FEATURE_DATA_D3D12_OPTIONS5 options = {};
m_device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &options, sizeof(options));
if (SUCCEEDED(hr) && options.RaytracingTier != D3D12_RAYTRACING_TIER_NOT_SUPPORTED) {
std::cout << "Raytracing is supported." << std::endl;
}
さらに、DXRではDirectX12の新しい機能を使う必要があるため、ID3D12Device5
とID3D12GraphicsCommandList4
に変更します。
Acceleration Structure (03_acceleration_structure)
ここから本格的にDXRの機能に入っていきます。まずはAcceleration Structureです。簡単に言えば、「衝突判定を素早く行うためのデータ構造」のことで、3D空間のオブジェクトの情報(ポリゴン・座標など)を保存します。
Acceleration Structureは、BVH (Bounding Volume Hierarchy) という技術を用いて実現されています。衝突判定の計算量を減らすために、まず大きなオブジェクトに対して衝突判定を行い、衝突したらもっと小さい範囲に対して衝突判定を行い...というように、階層に分けて衝突判定をすることで、無駄な判定を減らしています。
Acceleration Structureには以下の2つがあります。
- Bottom Level Acceleration Structure (BLAS): 個々のオブジェクトを管理する
- Top Level Acceleration Structure (TLAS): 複数のBLASをまとめて、シーン全体を管理する
BLAS
まずはBLASを作りましょう。
BLASはオブジェクトごとに作成します。今回は三角形だけなので1つです。まず、対象のジオメトリ情報を取得します。これは、頂点バッファから取得されます。
D3D12_RAYTRACING_GEOMETRY_DESC geometryDesc = {
.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES,
.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE,
.Triangles = {
.VertexFormat = DXGI_FORMAT_R32G32B32_FLOAT,
.VertexCount = static_cast<UINT>(m_vertices.size()),
.VertexBuffer = {
.StartAddress = m_vertexBuffer->GetGPUVirtualAddress(),
.StrideInBytes = sizeof(DirectX::XMFLOAT3)
},
}
};
BLASの基礎となるジオメトリには、三角形(今回利用したもの)のほかにAABBを指定することも可能です。それ以外の形状はシェーダーで衝突判定コードを書く必要があります。
BLASの作成では、ターゲットのバッファとスクラッチバッファが必要になります。スクラッチバッファは、BLASを作る過程で一時的に必要になる作業領域です。これらのサイズは、デバイスに問い合わせることで取得できます。
D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO asPrebuildInfo = {};
m_device->GetRaytracingAccelerationStructurePrebuildInfo(&asInputs, &asPrebuildInfo);
asPrebuildInfo
の中にResultDataMaxSizeInBytes
などの情報が入っているので、これらを用いてターゲットのバッファとスクラッチバッファを作成します。
そうしたら、BLASを構築します。BLASの構築はGPUで行われるので、バッファのGPUアドレスをコマンドに渡します。
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC blasDesc = {
.DestAccelerationStructureData = m_blas->GetGPUVirtualAddress(),
.Inputs = asInputs,
.ScratchAccelerationStructureData = blasScratch->GetGPUVirtualAddress()
};
m_commandList->BuildRaytracingAccelerationStructure(&blasDesc, 0, nullptr);
GPUの処理は非同期で、順番は確定していません。この後TLASを作成することになりますが、TLASの作成はBLASの作成が終わってから行う必要があります。そのため、UAVバリアを用いて、BLASが構築されるまで他のコマンドからのアクセスをブロックします。
D3D12_RESOURCE_BARRIER barrier = {
.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV,
.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE,
.UAV = { .pResource = m_blas.Get() }
};
m_commandList->ResourceBarrier(1, &barrier);
これでBLASが完成しました。
TLAS
次にTLASです。BLASではACCELERATION_STRUCTURE_INPUTS
にはgeometryDesc
というものを入れていましたが、TLASではinstanceDesc
というものを入れます。
D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS tlasInputs = {
.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL,
.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE,
.NumDescs = 1,
.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY,
};
instanceDesc
はGPU上のリソースとして作成するのでコード上では後の方で編集していますが、以下のようになります。
instanceDesc[0].InstanceID = 0;
instanceDesc[0].InstanceMask = 0xFF;
instanceDesc[0].InstanceContributionToHitGroupIndex = 0;
instanceDesc[0].Flags = D3D12_RAYTRACING_INSTANCE_FLAG_NONE;
instanceDesc[0].AccelerationStructure = m_blas->GetGPUVirtualAddress();
DirectX::XMFLOAT3X4 transformMatrix;
DirectX::XMStoreFloat3x4(&transformMatrix, DirectX::XMMatrixIdentity());
memcpy(instanceDesc[0].Transform, &transformMatrix,
sizeof(transformMatrix));
InstanceMask、 InstanceContribution...のフィールドは今は無視してください。今回はインスタンスは1つだけなので、instanceDesc
も1つです。フィールドAccelerationStructure
にて、BLASを設定し、Transform
にて、オブジェクトの座標や回転を設定しています(今回は原点)。
これで、シーン内のどこにどのBLASがいくつあるかという情報を得ることができ、この情報をもとにTLASを作成します。
TLASの構築はBLASの時と同じです。GetRaytracingAccelerationStructurePrebuildInfo
で問い合わせたサイズをもとにバッファを作成し、m_commandList->BuildRaytracingAccelerationStructure
を呼び出し、そしてUAVバリアを設定します。
これでTLASも完成しました。
...長いですね。ラスタライズパイプラインだと、今はまだ頂点バッファの作成くらいしかできていません。頑張っていきましょう。
シェーダー (04_shader)
ここからパイプラインを作っていきます。レイトレーシングのパイプラインは、従来のラスタライズパイプラインとはかなり違います。
ラスタライズパイプラインでは、頂点シェーダー、ピクセルシェーダー(+α)がありましたが、レイトレーシングパイプラインでは以下の5つのシェーダーが用いられます。
- RayGenerationシェーダー
- 光線(レイ)を飛ばす
- Missシェーダー
- レイがどんなオブジェクトともぶつからなかったときの処理(描画する色など)を記述する
- ClosestHitシェーダー
- レイがオブジェクトとぶつかったときの処理を記述する。反射方向を計算して別のレイを飛ばしたりもする
- (オプション) Intersectionシェーダー
- 三角形ではない形状に対して、独自に衝突判定処理を記述する
- プロシージャルジオメトリの作成に使用される
- (オプション) AnyHitシェーダー
- オブジェクトと衝突したときに呼び出され、レイのパラメータを変えたりClosestHitシェーダーの呼び出しをスキップしたりできる
IntersectionシェーダーとAnyHitシェーダーはオプションなので、今回はそれ以外の3つのシェーダーについて扱います。これらは、以下のようなフローで呼び出されます。
では、シェーダーを書きます。
RaytracingAccelerationStructure sceneAS : register(t0);
RWTexture2D<float4> output : register(u0);
[shader("raygeneration")]
void RayGen() {
uint3 dispatchIndex = DispatchRaysIndex();
output[dispatchIndex.xy] = float4(0.0f, 0.0f, 0.0f, 1.0f); // Clear output to black
}
struct Payload {
bool hit;
};
[shader("miss")]
void MissShader(inout Payload payload) {
payload.hit = false;
}
[shader("closesthit")]
void ClosestHitShader(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr) {
payload.hit = true;
}
まずはリソースの定義です。1行目にあるsceneAS
はTLASのことです。SRVとして扱います。2行目は画面出力で、UAVとして扱います。ComputeShaderを使ったことがあれば理解しやすいかもしれません。
4行目以降がシェーダーになります。シェーダーの各関数には[shader("raygeneration")]
のようにタグが付けられていますが、これによってシェーダーの種類を判定します。この例では、RayGeneration、 Miss、 ClosestHitシェーダーがそれぞれ1つずつと、最も簡単な構成です。ちなみに、MissシェーダーやClosestHitシェーダーは複数作成できます。たとえば、水面に対するClosestHitシェーダーでは光の屈折と反射を考慮し、地面のClosestHitシェーダーでは反射を弱めにする...などの使い分けができます。
シェーダーコードの中身を見ると、そもそもRayGenerationシェーダーでレイを飛ばしておらず、直接出力に書き込んでいることがわかります。レイを飛ばすにはDispatchRays
という関数を呼び出すのですが、その設定は結構長くなるのでいったん後回しにします。
しかし、RayGenerationシェーダーの中にも重要なポイントがあります。それは、dispatchIndexです。レイトレーシングの仕組みのところで、レイは視点からスクリーン上のあるピクセルをめがけて放たれると書きました。この目標のピクセル座標がdispatchIndexとなります。
Payload構造体は、レイを飛ばすときに持っている情報です。今回はヒットしたかどうかの情報しか持っていませんが、ここに色の情報を持っておけば、複数のオブジェクトに衝突したらその色をブレンドする、などの処理に使用できます。
DXIL Library
では、これらのシェーダーをコンパイルしてパイプラインに登録します。パイプラインについては次の章で書きますが、ラスタライズパイプラインで使っていたID3D12PipelineState
は使えず、ID3D12StateObject
を使うことになります。
シェーダーをコンパイルするわけですが、DXRは新しい機能のため<d3dcompiler.h>
は使えません。コマンドライン版DXC(DirectX Shader Compiler)であれば対応しているので、それを用いてHLSLを中間表現であるDXILに変換します。コマンドラインツールを使ってもいいですが、ここではそのDXCをプログラムから呼び出せるAPIを使用します。vcpkgでdirectx-dxc
と提供されているものを使用します。
では使っていきましょう。基本的にはコマンドラインツールをただプログラム中から使っている感じで、初期化→ファイル読み込み→コンパイルという流れになります。
まず初期化です。
Microsoft::WRL::ComPtr<IDxcCompiler3> compiler;
Microsoft::WRL::ComPtr<IDxcUtils> utils;
DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler));
DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));
Microsoft::WRL::ComPtr<IDxcIncludeHandler> includeHandler;
utils->CreateDefaultIncludeHandler(&includeHandler);
そして、ファイルの読み込みをします。
Microsoft::WRL::ComPtr<IDxcBlobEncoding> sourceBlob;
utils->LoadFile(SHADER_FILE.c_str(), nullptr, &sourceBlob);
DxcBuffer sourceBuffer = {
.Ptr = sourceBlob->GetBufferPointer(),
.Size = sourceBlob->GetBufferSize(),
.Encoding = DXC_CP_ACP
};
できたら、コンパイルをします。
std::array args = {SHADER_FILE.c_str(), L"-T", L"lib_6_3"};
Microsoft::WRL::ComPtr<IDxcResult> result;
compiler->Compile(
&sourceBuffer,
args.data(),
args.size(),
includeHandler.Get(),
IID_PPV_ARGS(&result)
);
Microsoft::WRL::ComPtr<IDxcBlob> shaderBlob;
Microsoft::WRL::ComPtr<IDxcBlobUtf16> shaderName;
result->GetOutput(DXC_OUT_OBJECT, IID_PPV_ARGS(&shaderBlob), &shaderName);
実際にはエラー処理や、コンパイルで出力されたエラーの表示などがあるため、実際のコードはもう少し長くなっています。
Compile
メソッドの引数を見れば、基本的にコマンドラインツールをただ使っているだけというのがわかるかと思います。
最後に、これをパイプラインに登録します。パイプラインには、STATE_SUBOBJECT
という形式で登録します(次章で書きます)。
std::array exportDescs = {
D3D12_EXPORT_DESC{
.Name = RAYGEN_SHADER.c_str(),
.ExportToRename = nullptr,
.Flags = D3D12_EXPORT_FLAG_NONE
},
D3D12_EXPORT_DESC{
.Name = MISS_SHADER.c_str(),
.ExportToRename = nullptr,
.Flags = D3D12_EXPORT_FLAG_NONE
},
D3D12_EXPORT_DESC{
.Name = CLOSEST_HIT_SHADER.c_str(),
.ExportToRename = nullptr,
.Flags = D3D12_EXPORT_FLAG_NONE
}
};
D3D12_DXIL_LIBRARY_DESC dxilLibraryDesc = {
.DXILLibrary = {
.pShaderBytecode = shaderBlob->GetBufferPointer(),
.BytecodeLength = shaderBlob->GetBufferSize()
},
.NumExports = static_cast<UINT>(exportDescs.size()),
.pExports = exportDescs.data()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY,
.pDesc = &dxilLibraryDesc
};
exportDescs
で、コンパイルしたシェーダー内のエントリーポイントを設定しています。
パイプラインの作成 (05_pipeline)
いよいよレイトレーシング用のパイプラインを作成していきます。
パイプラインには、以下の設定が必要です。
- シェーダー(DXIL Library)
- Hit Group
- Shader Config
- Pipeline Config
- Global Root Signature
- Local Root Signature
レイトレーシング用のパイプラインでは、ID3D12PipelineState
ではなく、ID3D12StateObject
を使用します。また、上記の設定項目の一つ一つはD3D12_STATE_SUBOBJECT
構造体で表され、その配列をID3D12StateObject
に渡してパイプラインを作成します。
この説明からもわかるように、STATE_SUBOBJECTの配列は可変長です。たとえば、Local Root Signatureは数が増えることがよくあります。
Hit Group
では、1つずつ作っていきます。DXIL Libraryは先ほど作ったので、次はHit Groupです。
Hit Groupとは、Closest Hit Shader、 Any Hit Shader、 Intersection Shaderをまとめたもので、レイがオブジェクトに当たった時に実行するシェーダーを指定します。今回は1つしか作りませんが、複数のHit Groupを作って「オブジェクト用のHit Group」「地面用のHit Group」を作って、オブジェクトごとに実行するシェーダーをHit Group単位で分けることができます。
D3D12_HIT_GROUP_DESC hitGroupDesc = {
.HitGroupExport = HIT_GROUP.c_str(),
.ClosestHitShaderImport = CLOSEST_HIT_SHADER.c_str(),
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP,
.pDesc = &hitGroupDesc
};
subobjectIndex++;
ここでは、Closest Hit ShaderのみをHit Groupに登録しています。
Shader Config
次にShader Configです。これはその名の通りシェーダーの設定で、シェーダー内で使用するデータのサイズを指定します。Payloadの方はシェーダーで作ったPayload構造体のサイズを指定します。4バイト単位なので、今回は直接指定しました。Attributeの方はClosest Hitシェーダーで使われていたBuiltInTriangleIntersectionAttributes
構造体のサイズを指定します。定義はDirectX Specsに書いてある[5]ので、それと同じものをC++側に用意しました。
D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {
.MaxPayloadSizeInBytes = sizeof(float),
.MaxAttributeSizeInBytes = sizeof(BuiltInTriangleIntersectionAttributes)
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG,
.pDesc = &shaderConfig
};
subobjectIndex++;
また、Shader ConfigではExports Assosiationというものも設定する必要があります。これは、SUBOBJECTで指定した設定と実際のシェーダーを結びつける設定で、今回の場合はすべてのシェーダーに対してShader Configを設定するので、そのようにExports Assosiationを設定します。
std::array exportNames = {RAYGEN_SHADER.c_str(), MISS_SHADER.c_str(), CLOSEST_HIT_SHADER.c_str()};
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION subobjectToExportsAssociation = {
.pSubobjectToAssociate = &subobjects[subobjectIndex - 1],
.NumExports = static_cast<UINT>(exportNames.size()),
.pExports = exportNames.data()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION,
.pDesc = &subobjectToExportsAssociation
};
subobjectIndex++;
Pipeline Config
続いてPipeline Configです。これは設定することはあまり多くはありません。このMaxTraceRecursionDepth
というのは、レイをトレースする回数を指定します。今はレイを飛ばしていないので関係ありませんが、反射シェーダーなどを実装すると「オブジェクトに当たったら反射方向に次のレイを飛ばす」というように、レイを再帰的に飛ばすことができます。しかし、向かい合った鏡面などにレイが当たった場合、鏡面の間を無限に反射し続ける可能性があります。これを防ぐため、レイを飛ばす最大数を設定する必要があるのです。
私は、ここを0(すなわち、制限なし)に設定した結果、GPUが無限ループに陥り、クラッシュしてディスプレイが真っ暗になりました。とても怖いので気をつけてください。
D3D12_RAYTRACING_PIPELINE_CONFIG pipelineConfig = {
.MaxTraceRecursionDepth = 2
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG,
.pDesc = &pipelineConfig
};
subobjectIndex++;
Root Signature
最後はRoot Signatureです。レイトレーシングパイプラインで使うRoot Signatureには2種類あり、Local Root SignatureとGlobal Root Signatureがあります。これらは、Root Signatureが適用されるシェーダーによって分類されます。Global Root Signatureはその名の通りグローバルなRoot Signatureで、ここでバインドされたリソースはすべてのシェーダーで利用できます。一方Local Root Signatureは適用するシェーダーの種類を制限することができます。そのため、例えば「オブジェクトのClosest Hit Shaderにはテクスチャリソースをバインドする」「地面のClosest Hit Shaderにはテクスチャに加えて法線マップもバインドする」などのようにシェーダーによってバインドするリソースを変えることが可能になります。
今回の場合、シェーダーに記述したリソースはTLASと出力バッファの2つですが、それらはどちらもRayGeneration Shaderでしか使われていません。よって、ここではGlobal Root Signature、 Miss/Closest Hit Shader用のLocal Root Signatureには何もバインドせず、RayGeneration Shaderに2つのリソースをバインドするようにしましょう。
作成方法は普通のRoot Signatureと同じで、Root Paramaterの定義→SerializeRootSignature→CreateRootSignatureという流れになります。
Global Root Signatureは中身がないので、作成は省略してSUBOBJECTの定義を書いておきます。
D3D12_GLOBAL_ROOT_SIGNATURE globalRootSignature = {
.pGlobalRootSignature = rootSignature.Get()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE,
.pDesc = &globalRootSignature
};
subobjectIndex++;
続いてRayGeneration Shader用のRoot Signatureです。これも通常のRoot Signatureと同じように作成します。
Local Root Signatureでは、どのシェーダーに対して適用するかを指定する必要があるため、EXPORTS_ASSOSIATIONも作ります。
std::array ranges = {
D3D12_DESCRIPTOR_RANGE{
.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
.NumDescriptors = 1,
.BaseShaderRegister = 0,
.RegisterSpace = 0,
.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND
},
D3D12_DESCRIPTOR_RANGE{
.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_UAV,
.NumDescriptors = 1,
.BaseShaderRegister = 0,
.RegisterSpace = 0,
.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND
},
};
// ...省略...
D3D12_LOCAL_ROOT_SIGNATURE localRootSignature = {
.pLocalRootSignature = raygenRootSignature.Get()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE,
.pDesc = &localRootSignature
};
subobjectIndex++;
std::array raygenExportNames = { RAYGEN_SHADER.c_str() };
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION raygenSubobjectToExportsAssociation = {
.pSubobjectToAssociate = &subobjects[subobjectIndex - 1],
.NumExports = static_cast<UINT>(raygenExportNames.size()),
.pExports = raygenExportNames.data()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION,
.pDesc = &raygenSubobjectToExportsAssociation
};
subobjectIndex++;
最後にMiss/Closest Hit Shader用のRoot Signatureです。Root Signatureの作成自体はGlobal Root Signatureと同じなので省略して、EXPORTS_ASSOSIATIONのみを書きます。
std::array missHitExportNames = { MISS_SHADER.c_str(), CLOSEST_HIT_SHADER.c_str() };
D3D12_SUBOBJECT_TO_EXPORTS_ASSOCIATION missHitSubobjectToExportsAssociation = {
.pSubobjectToAssociate = &subobjects[subobjectIndex - 1],
.NumExports = static_cast<UINT>(missHitExportNames.size()),
.pExports = missHitExportNames.data()
};
subobjects[subobjectIndex] = D3D12_STATE_SUBOBJECT{
.Type = D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION,
.pDesc = &missHitSubobjectToExportsAssociation
};
subobjectIndex++;
パイプラインの作成
これですべての要素が準備できたので、いよいよ作成します。レイトレーシングパイプラインはID3D12StateObject
なので、CreateStateObject
メソッドを使用します。
D3D12_STATE_OBJECT_DESC stateObjectDesc = {
.Type = D3D12_STATE_OBJECT_TYPE_RAYTRACING_PIPELINE,
.NumSubobjects = static_cast<UINT>(subobjectIndex),
.pSubobjects = subobjects.data()
};
m_device->CreateStateObject(&stateObjectDesc, IID_PPV_ARGS(&m_raytracingPipelineState));
Shader Table (06_shader_table)
先ほど、Local Root Signatureによってシェーダーごとに特有のリソースバインディングを定義しましたが、あくまでもRoot Signatureは「シェーダーが使用できるリソースの情報」を渡しているだけで、実際のリソースとのかかわりは何も定めていません。そこで使用するのがShader Tableです。Shader Tableでは、個々のシェーダーと、それが使うリソースの存在(Root ParameterをDescriptor Tableで定義していた場合、そのDescriptor Handle)とを紐づけるものです。
リソースの作成
Shader Tableを作る前に.そもそもリソースを作っていなかったので作ります。シェーダーにもある通り、今回使用するのはTLASと出力バッファの2つです。TLAS自体はできているので、TLASのSRVと出力バッファを作っていきます。
D3D12_DESCRIPTOR_HEAP_DESC heapDesc = {
.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV,
.NumDescriptors = 2, // | tlas | output texture |
.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE,
.NodeMask = 0
};
hr = m_device->CreateDescriptorHeap(&heapDesc, IID_PPV_ARGS(&m_descHeap));
// 出力バッファの作成(省略)
D3D12_CPU_DESCRIPTOR_HANDLE srvHandle = m_descHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc ={
.Format = DXGI_FORMAT_UNKNOWN,
.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE,
.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING,
.RaytracingAccelerationStructure = {
.Location = m_tlas->GetGPUVirtualAddress()
}
};
m_device->CreateShaderResourceView(nullptr, &srvDesc, srvHandle);
srvHandle.ptr += m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {
.Format = DXGI_FORMAT_R8G8B8A8_UNORM,
.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D,
.Texture2D = {
.MipSlice = 0,
}
};
m_device->CreateUnorderedAccessView(m_raytracingOutput.Get(), nullptr, &uavDesc, srvHandle);
出力バッファは、単純にウィンドウサイズで2Dバッファを作っているだけなので省略しました。UAVを作成するのでFlagにD3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS
を指定する必要があります。
それ以降は、TLASのSRVと出力バッファのUAVを作成しています。SRVのDimentionにはTLAS専用のD3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE
を指定します。
Shader Tableの作成
Shader Tableは、Shader Recordと呼ばれるものの集まりです。Shader Recordは、各シェーダーを表すShader Identifierと、Local Root Signatureで設定したリソースへのハンドルによって構成されます。このリソースへのハンドルはRoot Parameterにつき1つになります。ラスタライズパイプラインでSetGraphicsRootDescriptorTable
を指定していたのと同じようなものだと思うとわかりやすいかもしれません。
Shader Identifierは、レイトレーシングパイプラインから取得したID3D12StateObjectProperties
から取得できます。
Microsoft::WRL::ComPtr<ID3D12StateObjectProperties> stateObjectProps;
m_raytracingPipelineState->QueryInterface(IID_PPV_ARGS(&stateObjectProps));
stateObjectProps->GetShaderIdentifier(RAYGEN_SHADER.c_str())
また、Shader Table内のShader Recordは大きさをそろえる必要があります。今回は、RayGen用のLocal Root SignatureのみRoot Descriptor Tableが1つあるので、そのサイズに合わせます。
m_shaderRecordSize = align(
D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES + sizeof(D3D12_GPU_DESCRIPTOR_HANDLE),
D3D12_RAYTRACING_SHADER_RECORD_BYTE_ALIGNMENT
);
UINT totalSize = m_shaderRecordSize * 3; // raygen、 miss、 hitgroup
Shader TableはGPU上のリソースとなるため、UPLOADバッファとして作成しておいて、Shader Recordをmemcpyによって入力します。
// raygen
memcpy(mappedData, stateObjectProps->GetShaderIdentifier(RAYGEN_SHADER.c_str()), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
D3D12_GPU_DESCRIPTOR_HANDLE descHandle = m_descHeap->GetGPUDescriptorHandleForHeapStart();
memcpy(mappedData + D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES, &descHandle, sizeof(D3D12_GPU_DESCRIPTOR_HANDLE));
mappedData += m_shaderRecordSize;
// miss
memcpy(mappedData, stateObjectProps->GetShaderIdentifier(MISS_SHADER.c_str()), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
mappedData += m_shaderRecordSize;
// hitgroup
memcpy(mappedData, stateObjectProps->GetShaderIdentifier(HIT_GROUP.c_str()), D3D12_SHADER_IDENTIFIER_SIZE_IN_BYTES);
mappedData += m_shaderRecordSize;
コマンド実行 (07_command)
では、いよいよ描画コマンドを実行します。レイトレーシングパイプラインにおける描画では、
- UAVである出力バッファに色を書き込む
- 出力バッファをバックバッファにコピーする
という流れで描画を行います。そのため、それを達成するためのTransition Barrierを設定します(ここでは省略)。
その後、D3D12_DISPATCH_RAYS_DESC
構造体を設定します。これはShader Tableの設定を決めます。今回はShader Tableが3つしかないので単純です。
D3D12_DISPATCH_RAYS_DESC dispatchDesc = {
.RayGenerationShaderRecord = {
.StartAddress = m_shaderTable->GetGPUVirtualAddress(),
.SizeInBytes = m_shaderRecordSize
},
.MissShaderTable = {
.StartAddress = m_shaderTable->GetGPUVirtualAddress() + m_shaderRecordSize,
.SizeInBytes = m_shaderRecordSize,
.StrideInBytes = m_shaderRecordSize
},
.HitGroupTable = {
.StartAddress = m_shaderTable->GetGPUVirtualAddress() + m_shaderRecordSize * 2,
.SizeInBytes = m_shaderRecordSize,
.StrideInBytes = m_shaderRecordSize
},
.Width = static_cast<UINT>(m_windowRect.right - m_windowRect.left),
.Height = static_cast<UINT>(m_windowRect.bottom - m_windowRect.top),
.Depth = 1
};
そうしたら、ラスタライズパイプラインと同様にDescriptorHeap、 RootSignature、 Pipelineの設定をして、ラスタライズパイプラインにおけるDraw
関数に相当するDispatchRays
を呼び出します。
std::array descHeaps = { m_descHeap.Get() };
m_commandList->SetDescriptorHeaps(descHeaps.size(), descHeaps.data());
m_commandList->SetComputeRootSignature(m_globalRootSignature.Get());
m_commandList->SetPipelineState1(m_raytracingPipelineState.Get());
m_commandList->DispatchRays(&dispatchDesc);
最後にCopyResource
をして終了です。
これで、真っ暗な画面が表示されると思います。シェーダーコードに書いた色の値を変えるとちゃんと出力も変わるので、シェーダーを認識していることがわかります。
三角形を表示する (08_triangle)
では、最後に三角形を表示しましょう。先ほどまではレイトレーシングといいながらレイを1つも飛ばしていませんでしたが、今回はちゃんとレイを飛ばして図形を描画します。
まず、シェーダーを書き換えます。今回は色を扱うので、Payloadにはcolorを持たせました。
struct Payload {
float4 color;
};
そして一番大きく変わるのがRayGenerationシェーダーです。
[shader("raygeneration")]
void RayGen() {
uint2 dispatchIndex = DispatchRaysIndex().xy;
uint2 targetSize = DispatchRaysDimensions().xy;
float2 uv = (float2(dispatchIndex) / float2(targetSize)) * 2.0f - 1.0f;
uv.y = -uv.y;
RayDesc ray;
ray.Origin = float3(0.0f, 0.0f, -2.0f); // Camera position
ray.Direction = normalize(float3(uv, 1.0f)); // Ray direction
ray.TMin = 0.001f;
ray.TMax = 1000.0f;
Payload payload;
payload.color = float4(0.0f, 0.0f, 0.0f, 1.0f);
TraceRay(sceneAS, RAY_FLAG_NONE, 0xFF, 0, 0, 0, ray, payload);
output[dispatchIndex] = payload.color;
}
最初の5行程度で、レイを飛ばす目標となるピクセルのuv座標を計算します。dispatchIndexは(100, 150)のように整数値で与えられるので、それを[-1.0, 1.0]の範囲に正規化します。Y座標を反転させているのは、dispatchIndexは下向きを正として計算しているためです。
最も重要なのがRayDesc
構造体とTraceRay
関数です。RayDesc
構造体は飛ばすレイのパラメータを決めます。このパラメータによって、レイの方程式は
となります。今回の場合、視点をにおき、スクリーンはにあるものを想定しています。
続いてTraceRay
関数です。定義[6]は以下のようになっています。
void TraceRay(RaytracingAccelerationStructure AccelerationStructure,
uint RayFlags,
uint InstanceInclusionMask,
uint RayContributionToHitGroupIndex,
uint MultiplierForGeometryContributionToHitGroupIndex,
uint MissShaderIndex,
RayDesc Ray,
inout payload_t Payload);
なんとかIndexという部分は、オブジェクトやインスタンスが複数ある場合にどのHitGroupやMissShaderを適用するかという設定に使用するので、今回は考えません。今回考えるのは、AccelerationStructure
とRay
とPayload
です。AccelertionStructure
はTLASを渡します。Ray
には先ほど作成したRayDesc
を渡します。そしてPayload
も渡します。この引数はinoutとついている通り、レイが進む過程で書き込まれていきます。そのため、基本的にこのPayload
の色を出力することにします。
MissShaderとClosestHitShaderを見ましょう。
[shader("miss")]
void MissShader(inout Payload payload) {
payload.color = float4(0.0f, 0.2f, 0.8f, 1.0f);
}
[shader("closesthit")]
void ClosestHitShader(inout Payload payload, in BuiltInTriangleIntersectionAttributes attr) {
float u = attr.barycentrics.x;
float v = attr.barycentrics.y;
float w = 1.0f - u - v;
payload.color = float4(u, v, w, 1.0f);
}
MissShaderはオブジェクトにヒットしなかったときの色を定義するので、これが背景色となります。実際の描画だと、空の色などを設定することになると思います。
ClosestHitShaderで計算している(u, v, w)は、それぞれ三角形の各頂点からの重みを表します。例えば、ある頂点上ではになりますし、重心ではになります。これは後の出力された画像を見るとわかりやすいと思います。
さて、シェーダーができたら、C++側のPayloadを書き直して実行です。すでに三角形のBLASは作ってあるので、これで表示できます。
できました!全然レイトレーシングをやっている感じはしないですが、とにかく三角形を表示することができました。
おわりに
とても長くなってしまいました。リアルタイムレイトレーシングはラスタライズパイプラインと比べてもやることが多い印象で、特にシェーダーの書き方が全く異なるので慣れるまでには時間がかかりそうだなと感じていました。レイトレーシングでモデルなどを表示できるようになるにはまだまだ先は長そうです。頑張って勉強しないとですね。
それでも、DirectX-Graphics-SamplesのD3D12RaytracingProcedualGeomtryサンプルを実行してみると結構感動するので、興味があったら是非やってみてください。
明日の投稿者は@SAH123さん,@Delphyさんです。楽しみ~
https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html ↩︎
"Some techniques for shading machine renderings of solids"、 Arthur Appel https://dl.acm.org/doi/10.1145/1468075.1468082 ↩︎
https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html#intersection-attributes-structure ↩︎
https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html#traceray ↩︎