NVIDIAのDirectX Raytracing Tutorialsを見てみる

せっかくなので、NVIDIAのDirectX Raytracing Tutorialsを見てみたいと思います。
GitHubのリポジトリは以下の通りです。
https://github.com/NVIDIAGameWorks/DxrTutorials

#01 CreateWindow

まず初めにWindowの作成と表示からです。レンダリングは何もしません。このチュートリアルは、サンプルコードのプロジェクトの番号を進めるにしたがって、次第にソースコードを追加していき、最終的にDXRのレンダリングを達成するものになっています。したがって、順を追ってソースコードを見ていけば自然と理解が深まるように作られています。
このサンプルコードのアプリケーションクラスの本体は、Falcor::Rendererクラスを継承しており、アプリケーション自身のコードはほとんどありません。
ちなみに、FalcorはNVIDIAのResearcherが使う共通のRealtime Rendering Frameworkです。アプリケーションの作成に際し、最低限以下のメソッドをOverrideする必要があるので、その部分はアプリケーション側に記述があります。

void onLoad(SampleCallbacks* pSample, RenderContext::SharedPtr pRenderContext) override;
void onFrameRender(SampleCallbacks* pSample, RenderContext::SharedPtr pRenderContext, Fbo::SharedPtr pTargetFbo) override;
void onShutdown(SampleCallbacks* pSample) override;
bool onMouseEvent(SampleCallbacks* pSample, const MouseEvent& mouseEvent) override;
bool onKeyEvent(SampleCallbacks* pSample, const KeyboardEvent& keyEvent) override;

Windowを作成して、ESCキー押下でPostQuitMessageします。

#2 InitDXR

このプロジェクトでは、D3D12のデバイスを作成して、実際にレンダリングのための準備を行います。
DXRが使用可能なDeviceを作成し、SwapChainとDirectCommandListのセットアップ等を行い、SwapChainをClearRTVするところまで実装します。

DXRが使用可能なDeviceの作成

Deviceの作成に先立ち、D3D12EnableExperimentalFeatures()に指定のUUID, D3D12RaytracingPrototypeを渡して、DXRの機能が使用可能か問い合わせます。
これが失敗するのは、主に以下の原因が考えられます。

  • OSが対応していない、またはDeveloperモードではない
  • D3D12.dll(および、このDLLから呼び出されるドライバー、使用中のGPUなど)がこの機能に対応していない

失敗する場合はDXRは使用できません。

次にD3D12CreateDevice()を呼び出します。FeatureLevelはD3D_FEATURE_LEVEL_12_0で問題無いようです。正しく対応していない環境だと、上記のEnableExperimentalFeaturesの呼び出しに成功しても、CreateDeviceの呼び出しでNullを返されることがありますので注意が必要です。

ちなみに、MicrosoftのDirectX-Graphics-Samplesに含まれる、D3D12Raytracingのサンプルコードでは、ComputeShaderによるFallbackLayerをサポートしていますが、こちらは、DXRの機能をComputeShaderとその機能拡張を用いて、サンプルコードの動作を実現しています。ただし一部の機能は残念ながらサポートされていないようです。一方で、NVIDIAのサンプルコードには、ComputeShaderによるFallbackLayerはサポートされていないので、実行にはDXRがサポートされた環境が必要です。2018/04/03現在では、Windows10RS4とTitan-Vそして、近日リリース予定のR396系のドライバーが必要です。現状では実行することができませんが、ソースはシンプルで読みやすいので、DXRのチュートリアルには向いていると思います。

その他のセットアップ

DxrSample::initDXR()に実装が記述されていますが、DXR特有の初期化は、上記のDevice作成時を除いて特にありません。このサンプルコードでは、Device, CommandQueue, SwapChain(buffer count=3)等をを作成し、3フレーム分のCommandAllocatorとRTV、最後にフレーム同期のためのFenceとEventObjectを作成しています。

レンダリング

SwapchainのRTVを設定して、ClearRTVを実行して、Present()するだけです。
今後チュートリアルを進めるにつれて、これらに実際のDXRのレンダリングを追加していきます。

#3 Acceleration Structure

このプロジェクトから、本格的にDXR特有のコードが記述されています。
まずはRaytracingの対象となる幾何形状を定義する、AccelerationStructure(以下AS)を作成するサンプルコードです。ちなみに、このプロジェクトではASを作成しますが実際に使用はしません。

ASの作成

ASの作成は大きく分けて3つに分かれます。

  • Triangles(ポリゴン)形状の場合は頂点バッファ(と必要ならインデックスバッファ)の作成。任意の形状の場合はAABBのリストを格納したバッファの作成
  • Bottom Level Acceleration Structureの作成
  • Top Level Acceleration Structureの作成

ASそのものは、現在のところBVH相当と考えて問題ありませんが、DXRはASの構造をBVHに限定していません。この点については留意が必要です。

Trianglesを格納したバッファの作成

いわゆる頂点バッファに相当します。これを、Bottom Level Acceleration Structureを作成する際に使用します。このサンプルコードでは3頂点で構成された三角形一つをUploadHeapに格納しているだけです。

Bottom Level Acceleration Structure(以下BLAS)の作成

上記で定義したTrianglesを格納するASを生成します。ちょうど、シーン上の一つのオブジェクトに相当するASを構築するイメージです。
まず、BLASの生成に先立ち、ASを格納することができるバッファの大きさをDeviceに問い合わせます。
GetRaytracingAccelerationStructurePrebuildInfo()に、先ほど生成した頂点バッファを指定して呼び出すと、3つのサイズを返します。それぞれ以下の意味があります。

  • ResultDataMaxSize
    ASの保持に必要なHeapのサイズになります
  • ScratchDataSize
    ASの生成時に必要になる一時Heapのサイズです
  • UpdateScratchDataSize
    ASを更新(頂点位置の移動)する際に必要になる一時Heapのサイズです

このサンプルコードでは、ASの生成のみを行うので、ResultDataMaxSizeとScratchDataSizeの二つのサイズのDefaultHeapを作成します。両者ともUAVを使用可能にするために、FLAG_ALLOW_UNORDERED_ACCESSを付けます。InitialStateは、ScratchDataのHeapには、STATE_UNORDERED_ACCESS、ResultDataのHeapには、STATE_RAYTRACING_ACCELERATION_STRUCTUREをつけています。
BuildRaytracingAccelerationStructure()を呼び出すと、BLASが作成されるのですが、こちらはCommandListのメソッドなので、GPU上で後程実行されます。

ちなみに、このサンプルコードでは使用されていませんが、D3D12_RAYTRACING_GEOMETRY_FLAG_NO_DUPLICATE_ANYHIT_INVOCATIONというフラグがBLASの生成時に指定可能です。DXRは基本的にAnyHitShaderを同一のTriangleに対して複数回呼び出すことを許容しています。またBVHの生成時に非常に大きな(細長い)Triangleを複数のBVHに格納することは、交差判定の効率上必要な事です。しかし一方で同一のプリミティブによる複数回のAnyHitShaderの呼び出しが起きると、アプリケーションによっては問題となる場合も考えられるため、このようなフラグが用意されています。当然ですが指定すれば交差判定の効率は低下すると思われます。また、ASに投入するTrianglesの形状もBVHの生成を意識したものにするのが望ましいと思われます。これは実際のモデルを作成するアーティストが意識する必要があります。

また、このサンプルコードでは使用されていませんが、EmitRaytracingAccelerationStructurePostBuildInfo()というコマンドを用いることで、生成されたASの様々な情報をGPU側から読み出すことができます。

  • COMPACTED_SIZE
    生成済のASを最適化されたサイズで格納する場合に必要なHeapのサイズです。
  • TOOLS_VISUALIZATION
    デバッグツールなどでVisualizationする際に必要になる情報を格納するために必要なHeapのサイズです
  • SERIALIZATION
    シリアライズする際に必要になるHeapのサイズです

これらの情報をもとに、Heapを作成して、CopyRaytracingAccelerationStructure()というコマンドを用いることで、最適化されたサイズへの変更(コピー)や、デバッグツールでのASの可視化、また、作成済のASのシリアライズ、デシリアライズを行うことができます。

Top Level Acceleration Structure(以下TLAS)の作成

TLASは、Raytracingを行うシーン全体を定義するものになります。複数のBLASのInstanceに4×3マトリクスを与えてシーン上に配置する役割があります。
このサンプルコードでは上記で定義したBLASを一つInstanceとして配置するだけです。
まず、TLASでもBLASと同様に、ASを格納することができるバッファの大きさをDeviceに問い合わせます。GetRaytracingAccelerationStructurePrebuildInfo()に、配置するInstanceの最大数を設定して呼び出すと、3つのサイズを返します。サイズの意味はBLASと同じです。DefaultHeapをそれぞれ確保します。

次に配置するInstanceのリストをHeapに作成します。Heapの中身は、D3D12_RAYTRACING_INSTANCE_DESCの配列となります。このサンプルコードでは、Instanceを一つ配置するだけなので、D3D12_RAYTRACING_INSTANCE_DESC一つ分のUploadHeapを確保して内容を設定します。今回は先に作成したBLASのGPU上のアドレスと配置場所のマトリクスを設定するだけです。
InstanceDescriptorは、ほかにもInstanceID(Shaderから参照可能)InstanceMask(RayのInclusionMaskとともに、ObjectがRayとインタラクトするかの設定)InstanceContributionToHitGroupIndex(このInstanceにRayがHitした(しそうな)時に参照されるShaderTableのオフセット値)それから、BackfaceCullingの制御(Rayのフラグとともに動作)とOpaqueの制御(これもRayのフラグとともに動作)と様々な機能があります。ただし、これらは今回のサンプルコードでは特に使用されていません。
また、BLASとTLASの作成を一連のCommandListで実行する場合は、ResourceBarrierが必要です。作成したBLASはTLASの作成時に参照されます。

#4 RtPipelineState

このサンプルプロジェクトでは、新たにRtPSOを作成します。ただし使いません。
D3D12のRasterizerのPSOは、GraphicsやComputeが使う様々なStateと一連のシェーダーステージに設定されるShaderを設定するためのものです。ShaderやStateの切り替えには、PSOの切り替えが必要です。
一方でRtPSOは、Stateと呼べるものはほんの僅かしかありません。また、RtPSOは内部にRaytracingで使用するすべてのShaderとRootSignatureを内包します。つまり、一つの巨大なShaderのセットとしての役割があります。これは、DXRがShaderをRayの状況に応じて切り替える必要があるからです。したがって、一度のDispatchRays()呼び出しで使用される可能性のあるすべてのShaderは、一つのRtPSOに内包される必要があり、その全てはRtPSOの作成時に指定する必要があります。
RtPSOの作成の方法は、以下のリストに示す項目をSubobjectとして先に作成し、これを一括でリストとして渡すことで、RtPSOを生成します。APIとしては極めて抽象的なつくりなので、注意が必要です。

Raytracing Pipeline Config

現在のところ、Rayの再帰生成の回数(~31)のみが定義されれています。
Rayは、RayGenのみでなく、ClosestHitやMissシェーダーからも再帰的に生成することができます。この再帰的な生成回数の上限を指定します。もちろんですが、必要最低限の数を設定することをお勧めします。このサンプルコードでは、Rayの再帰生成数は0に設定されています。
このSubobjectが、リストの中に複数あっても問題ないですが、値は同じである必要があります。

Raytracing Shader Config

PayloadとAttributeのサイズを定義します。せっかくなので、PayloadとAttributeについて簡単に説明します。

  • PayloadはRayGen, AnyHit, ClosestHit, Missシェーダーからアクセス可能なユーザー定義の構造体です。
    主に、Raytracingの結果(Radianceなど)を格納するために使います。
    Payloadのサイズの上限は現在ののところ明示的に示されていませんが、必要最小限にとどめることが望ましいです。
  • Attributeは、Intersectionシェーダーの結果をAnyHitとClosestHitシェーダーに伝えるためのユーザー定義(BuiltinのIntersection(後述)を使用する時はAPI側の定義)の構造体です。
    Triangleや様々な形状と交差が発生した場合に、具体的な交差位置や、それに付随する情報をHitシェーダーに伝えるために使います。Attributeのサイズの上限は、D3D12_RAYTRACING_MAX_ATTRIBUTE_SIZE_IN_BYTESで明示的に示されています。非常に小さいです。おそらく理由はAttribute情報をShader間で受け渡しする際に、ヒープを介さずに行うのではないかと推測されます。当然ですが、このサイズも必要最小限にとどめることが望ましいです。

一つのRtPSO内で使用するShaderの数に制限はありませんが、内部で使用するShaderは共通のPayloadサイズとAttributeサイズを用いる必要があります。
このSubobjectがRtPSO作成時のリストに複数あっても問題ないですが、値は同じである必要があります。このサンプルコードでは、Attributeがfloat*2, Payloadがfloat*1と設定されています。

Root Signature

こちらは従来のRootSignatureと同様の使い方になります。CommandListからRootArgumentsを介し、使用するShaaderResourceを指定して、RtPSO内のすべてのShaderからアクセスすることが可能になります。ちなみにDXRにはShaderStageという概念がないため、常にVISIBILITY_ALLで使用します。このサンプルコードでは、空のRootSignatureを一つ生成しています。

Local Root Signature

LocalRootSignatureは、ShaderTable(後述)より設定されるRootArgumentsのためのRootSignatureになります。
RayGen,HitGroup(AnyHit,ClosestHit,Intersectionのセット),Missのそれぞれで、各々のLocalRootSignatureを使用することができます。そのため、複数のLocalRootSignatureがSubobjectとして指定されていても問題ありません。一方でRootSignatureは、すべてのShaderで共通のものを使用します。LocalRootSignatureとRootSignatureのBindingは重複してはいけないルールになっています。
このサンプルコードでは、RayGenに一つ(UAVとSRV一つずつ。RenderTarget相当のUAVと、ASを指すSRV)と、HitGroupとMissに共通で一つ(空のRootSig)作成しています。

Hit Group

HitGroupはIntersection,AnyHit,ClosestHitの3つのシェーダーを一つのグループにしたものです。これらのシェーダーは、同一のLocalRootSignatureを用いて呼び出されるため、一つのグループにする必要があります。主にShaderのコンパイルのための都合です。
各ShaderはSubobjectとしてリストの中に存在する必要があり、これらは外部参照用の名前を持っているので、それを指定してHitGroupを作成します。さらに、このSubobject自身もHitGroupExportという、外部参照用の文字列を保持します。そのためHigGroupも他のSubobjectから参照可能になります。
Intersectionシェーダーは、BLASがTriangleのみの場合に限り省略可能です。その場合はBuiltinのIntersectionシェーダー(後述)が使用されます。AnyHit,ClosestHitも必要ない場合は省略可能です。
このサンプルコードでは、後述のDXILlibから読み込まれたClosestHitシェーダーを、外部参照名L”HitGroup”としてHitGroupとして作成しています。AnyHitとIntersectionシェーダーは省略されています。

DXIL library

DXIL libraryは内部に各種Subobjectを複数保持することができます。加えて各種Shaderを保持することができます。
DXIL libraryをSubobjectとして指定すると、内部に格納されているSubobjectが使用できます。このサンプルコードでは、RayGen,Miss,ClosestHitのShaderが格納されたDXILlibを指定しています。これは、プロジェクトに付属のHLSLファイルをdxcでコンパイルして生成したものです。これらのシェーダーは、すべて外部参照名を持っているので、前述のHitGroupの生成や後述のExports Associationで明示的に指定することができます。

Exports Association

Exports Associationは、あるSubobjectと外部参照名を関連づけるものです。
具体的な用途は、LocalRootSignatureや、ShaderConfigと各種Shaderを関連付けることで、Shaderが使用するLocalRootSignatureやPayload,Attributeのサイズを指定します。これらが決定することでShaderが実際にコンパイル可能になります。
このサンプルコードでは、DXILlibに格納されているRayGen,Missシェーダーと、先に作成したHitGroupに、それぞれLocalRootSignatureの関連付けと、ShaderConfigの関連付けを行っています。

RtPSOの作成

これら上記をすべて組み合わせて、使用する全てのShaderの定義、また、Shaderが使うLocalRootSignatureの定義、RootSignatureの定義、そしてPayloadとAttributeの大きさの定義、Rayの再帰生成の回数の定義を行い、RtPSOを生成します。実は、これらの条件は、ShaderをGPUのネイティブコードにコンパイルするために必要となる可能性のある情報なのではないかと思われます。RtPSO自体が、一つの大きなShaderプログラムオブジェクトと見なすことができると思います。

BuiltinのIntersectionシェーダーとAttributeについて

RtPSOと直接関係ありませんが、BuiltinのIntersectionシェーダーについて簡単に説明します。DXRでは、Triangle形状のBLASを走査する時は、BuiltinのIntersectionシェーダーを用いることができます。このサンプルコードでも実際にBuiltinのIntersectionシェーダーが使用されているため、”#07-Basic Shaders”が使用しているHLSLファイルを見ても、Intersectionシェーダーは記述されていません。この場合はDXR側がBLASと交差判定を行い、Hitした場合はfloat2のbarycentricsをAttributeとしてAnyHit/ClosestHitシェーダーに渡す約束になっています。Hitシェーダーは、InstnaceID,PrimitiveIDからHitしたTriangleを特定し、さらにAttributeで渡されたbarycentricsより、実際にHitした点を把握します。詳しくはシェーダーを解説する機会があれば説明したいと思います。

struct BuiltInTriangleIntersectionAttributes
{
    float2 barycentrics;
};

#05 ShaderBindingTable

このプロジェクトでは、ShaderTableを作成します。名前がShaderBindingTableとなっていますが、ShaderTableを作成します。
ShaderTableは、単一サイズのShaderRecordの配列で構成され、個々のShaderRecordは、ShaderIdentifierとそれに続くLocalRootArgumentを格納します。つまり、ShaderとそのShaderがローカルで使うShaderResourceを一緒に格納するためのものです。
ShaderIdentifierはRtPSO内でのShaderの識別子に相当し、RtPSOにShaderの外部参照名を渡すことで取得することができます。ShaderRecordに指定するShaderIdentifireはRayGen,Miss,HitGroupの識別子になります。つまりIntersection,AnyHit,ClosestHitは同一のShaderRecordを使用する必要があります。
LocalRootArgumentは、ShaderIdentifierで指定されたShaderに対応するLoclRootSignatureにマッチする内容を設定します。

ShaderTableの最も基本的な使い道は、TLASに設定された複数のBLASのInstanceごとに異なるShaderRecordを用意して、HitGroupを呼び出す際に各Instanceに対応したShaderRecordを指定することで、Instanceごとに異なるShaderResouce,つまりテクスチャやマテリアル等を用いてHitGroupを動作させることです。ShaderTableのオフセットの指定は、API側から、Shader側から、そしてAS側からと様々な場所からオフセットを指定することができます。実際にアクセスされるShaderRecordはこれらをすべて合算したものになります。全体のデザインはアプリケーションの設計に任されています。
またShaderRecordのサイズはShaderTableを通じて固定長となります。ですので、RtPSOの中に一つでも大きなLocalRootSignatureが存在すると、それがShaderTable全体のサイズに影響を及ぼすので注意が必要です。

ShaderTableは、単なるGPUから参照可能なHeap上に作成します。さらに言えば、ShaderTableは、UAV,SRVやCBVを介してBindingすることもなく、直接GPUアドレスとStrideを設定することで参照されます。このサンプルコードではUplaodHeapに作成したものをそのまま使っています。
このサンプルコードでは、合計3つのShaderRecordから構成されるShaderTableを作成しています。RayGenシェーダーのRecordのみ有効なLocalRootSignatureが指定されており、UAVとSRVをが格納されたDescriptorTableを格納したDescriptorHeapのアドレスを格納しています。HitGroupとMissシェーダーは、ShaderIdentifierのみ格納します。

#06 Raytrace

このプロジェクトでは、いよいよRayを飛ばします。DispatchRays()を呼び出すのですが、その前にいくつかやり残したことがあります。

RayGenのShaderRecordからアクセスされるDescriptorTableの設定

このサンプルコードでは、RayGenのみShaderResourceにアクセスしています。RayGenがアクセスする必要があるのは、Rayを走査する対象のTLASを格納しているHeapのSRVと、レンダリング結果を書き出すUAVです。ASのSRVは、作成する際のDimensionに、D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTUREを指定することで作成することができます。作成後は、通常のSRVと同じ方法でBindingすることができます。レンダリング結果を書き出すUAVは、従来と変わりありません。これらをRayGenシェーダーが参照するShaderRecordが指すDescriptorHeapに設定します。

D3D12_DISPATCH_RAYS_DESCの設定

Raytraceを開始するためのDispatchRays()の引数は2つあり、一つは先に作成したRtPSOで、もう一つは、D3D12_DISPATCH_RAYS_DESC構造体です。構造体のメンバーは以下のようになっています。

    D3D12_GPU_VIRTUAL_ADDRESS_RANGE RayGenerationShaderRecord;
    D3D12_GPU_VIRTUAL_ADDRESS_RANGE_AND_STRIDE MissShaderTable;
    D3D12_GPU_VIRTUAL_ADDRESS_RANGE_AND_STRIDE HitGroupTable;
    D3D12_GPU_VIRTUAL_ADDRESS_RANGE_AND_STRIDE CallableShaderTable;
    UINT Width;
    UINT Height;

まず、WidthとHeightですが、Width*Heightの数だけRayGenシェーダーがInvokeされます。RayGenerationShaderRecordは、Raytraceを開始するRayGenシェーダーのShaderRecordのアドレスとShaderRecordのサイズを指定します。
MissShaderTable,HitGroupShaderTableは、各シェーダーのShaderTableの先頭アドレスとサイズ、それからStrideを指定します。

DispatchRays()の呼び出し

RtPSOとD3D12_DISPATCH_RAYS_DESCを指定してDispatchRays()を呼び出すと、RayGenerationShaderRecordが指すShaderRecordのShaderIdentifierからRtPSO内のRayGenシェーダーを読みだし、続けて、LocalRootArgumentに設定されているShaderResourceをRayGenシェーダーからアクセス可能な状態にして、Width*Heightの数だけRayGenシェーダーがInvokeされます。
RayGenシェーダー内では、Rayの向きや開始点を計算し、Payloadの初期化を行い、Rayを走査するための組み込み関数を呼び出します。(シェーダーの詳しい仕組みは別の機会で解説したいと思います。)するとDXRがRayの走査を行い、走査の結果や過程で、MissやHitGroupシェーダーを呼び出します。その際の呼び出し対象のシェーダーは、インデックスで指定されます。Missの場合はRayGenShaderから明示的にインデックスを渡します。HitGroupの場合はインデックスの計算は複雑で、TLASに設定されたInstanceやBLAS、RayGenシェーダーからの設定などにより計算され、最終的なインデックスになります。これらのインデックスはShaderTableのエントリのオフセットとして使用されます。こうして算出された場所に格納されているShaderRecordより、ShaderIdentifierとLocalRootArgumentsが読み出され、実際に様々なシェーダーが動的に実行されるという仕組みになっています。CallableShaderTableはCallableシェーダーのためのShaderTableになりますが、今回は割愛させていただきます。

まとめ

今回はNVIDIAのDirectX Raytracing Tutorialsのサンプルプロジェクトの6番までをみて、実際にRaytraceが実行できるところまで見てみました。
次の機会があれば、このサンプルプロジェクトに含まれる他のコードか、Raytraceシェーダーについて見てみたい思います。

広告