DirectXの話 第152回

DirectX Raytracing Fallbackで3Dをやる その1 18/06/08 up

今回やったこと

    • オブジェクトを3D空間内に配置し、カメラを回転

    • 複数のBottom Acceleration Structureを使用

    • 複数のHitGroupを使用

    • 二次反射に対応

サンプルについて

いつもの場所に置いてあります。Sample02が今回のサンプルです。

https://github.com/Monsho/DXRSamples

なお、Fallback Layerも最新にしてください。かなり多くの修正が入ってます。

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

ソースコード解説

シェーダコード

シェーダコードは今回も test.r.hlsl ファイルとなります。

struct SceneCB { // ...};struct InstanceCB { // ...};struct Vertex { // ...};struct HitData { float4 color;}; RaytracingAccelerationStructure Scene : register(t0, space0); ByteAddressBuffer Indices : register(t1, space0); StructuredBuffer<Vertex> Vertices : register(t2, space0); RWTexture2D<float4> RenderTarget : register(u0); ConstantBuffer<SceneCB> cbScene : register(b0); ConstantBuffer<InstanceCB> cbInstance : register(b1); uint3 GetTriangleIndices2byte(uint offset){ // ...}

先頭からの数行です。定数バッファとシェーダリソース、構造体バッファなどの設定と構造体の宣言があります。

前回はオブジェクトの頂点バッファ、インデックスバッファは使いませんでしたが、今回はレイがヒットしたトライアングルの法線を求めるために設定が必要です。

t1レジスタにインデックスバッファ、t2レジスタに頂点バッファが設定されます。

今回、オブジェクトは複数存在するのですが、頂点バッファとインデックスバッファは1つにまとめています。

これについては後述します。

b0に設定している cbScene はシーンの情報を格納しており、ここではカメラの行列と位置、ライトの方向とカラーが納められます。

b1cbInstance はインスタンスごとに設定するデータで、インスタンスの回転行列とマテリアルカラー、頂点バッファとインデックスバッファの先頭オフセットが格納されています。

詳しくは後述します。

GetTriangleIndices2byte() 命令は指定のオフセット位置からトライアングルを形成するインデックスを3つ取得する命令です。

引数となるオフセット値は各インスタンスのインデックスオフセットと、ヒットしたトライアングルのプリミティブ番号から算出しています。

[shader("raygeneration")]void RayGenerator(){ uint2 index = DispatchRaysIndex(); float2 xy = (float2)index + 0.5; float2 clipSpacePos = xy / DispatchRaysDimensions() * float2(2, -2) + float2(-1, 1); float4 worldPos = mul(float4(clipSpacePos, 0, 1), cbScene.mtxProjToWorld); worldPos.xyz /= worldPos.w; float3 origin = cbScene.camPos.xyz; float3 direction = normalize(worldPos.xyz - origin); RayDesc ray = { origin, 0.0f, direction, 10000.0f }; HitData payload = { float4(0, 0, 0, 0) }; TraceRay(Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES, ~0, 0, 1, 0, ray, payload); RenderTarget[index] = payload.color;}

レイを生成する命令です。

前回は正射影でレイを飛ばしていましたが、今回は透視投影でレイを飛ばします。

まず最初の3行でクリップ空間での2D座標を求めます。

次に cbScene.mtxProjToWorld を用い、上記の2D座標を3Dワールド空間に変換します。

これによってワールド空間における座標が求められるので、カメラの位置からのベクトルを作成すれば、カメラから各ピクセルへのレイを求めることができます。

ここでは origin がレイの開始座標、direction がレイの方向です。

レイの情報を求めることができたので、RayDesc を生成して TraceRay() 命令に投げます。

これによってレイトレが開始されるのは前回の通りです。

[shader("closesthit")]void ClosestHitProcessorLambert(inout HitData payload : SV_RayPayload, in BuiltInTriangleIntersectionAttributes attr : SV_IntersectionAttributes){ uint indexOffset = PrimitiveIndex() * 2 * 3; uint3 indices = GetTriangleIndices2byte(indexOffset + cbInstance.ioffset * 2); float3 vertexNormals[3] = { // ... }; float3 normal = vertexNormals[0] + attr.barycentrics.x * (vertexNormals[1] - vertexNormals[0]) + attr.barycentrics.y * (vertexNormals[2] - vertexNormals[0]); normal = normalize(normal); normal = RotVectorByQuat(normal, cbInstance.quatRot); float NoL = saturate(dot(normal, -cbScene.lightDir.xyz)); float3 finalColor = cbInstance.matColor * cbScene.lightColor.rgb * NoL; float3 addColor = float3(0, 0, 0); if (!(RayFlags() & RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH)) { float3 origin = WorldRayOrigin() + WorldRayDirection() * RayTCurrent(); float3 reflection = dot(normal, -WorldRayDirection()) * 2.0 * normal + WorldRayDirection(); RayDesc ray = { origin, 1e-4, reflection, 10000.0f }; HitData reflPayload = { float4(0, 0, 0, 0) }; TraceRay(Scene, RAY_FLAG_CULL_BACK_FACING_TRIANGLES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH, ~0, 0, 1, 0, ray, reflPayload); addColor = reflPayload.color.rgb * 0.2f; } payload.color = float4(finalColor + addColor, 1);}[shader("closesthit")]void ClosestHitProcessorHalfLambert(inout HitData payload : SV_RayPayload, in BuiltInTriangleIntersectionAttributes attr : SV_IntersectionAttributes){ // ...}

closesthit シェーダは2つあります。それぞれマテリアルが異なるという設定で、上の命令はLambertディフューズ、下の命令は Half Lambertディフューズのマテリアルです。

最初にインデックスバッファからトライアングルの3つのインデックスを求めます。

3頂点のインデックスがわかれば、頂点を格納した構造体バッファから3頂点のノーマルを取得できます。

これらを取得できれば、barycentrics からヒットポジションのノーマルを求めることができます。

一旦、このノーマルを利用してライティングを行います。

次に、RayFlags() 命令で取得できるレイのフラグをチェックします。

このフラグに RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH が立っていない場合、このレイはカメラから飛ばした一次のレイと判断します。

この場合はレイがヒットした座標を二次レイの開始座標、ヒットした点のノーマルから完全反射ベクトルを求め、これを二次レイの方向とします。

この情報から二次レイの RayDesc を生成しますが、この時に開始位置を少し前方に進めています。(1e-4ほど)

これは、開始位置を 0 にした時に自分自身のトライアングルと再度ヒットしてしまうことがあったからです。

また、レイを飛ばす際には RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH フラグを立てます。

二次レイがオブジェクトにヒットした場合、このフラグを立てておくことでライティング計算のみ行われるようにしています。

DXRでは複数の closest hit shader を設定して、用途に合わせて切り替えることも可能です。

ただ、今回は二次レイによる鏡面反射をやっているだけなので、ライティング命令を再度別シェーダで実装するのは面倒でしたので同じシェーダを使い回しています。

最後に自身の直接ライトのカラーと、二次レイの結果を加算して Payload に流します。

下の命令はライティングモデルが Lambert から Half Lambert になっただけです。

miss shader は単色返すだけなので割愛。

パイプラインステートを作成する

デバイスの作成は前回と変化がありません。

まず大きく変わっているのはこのパイプラインステートでしょう。

Sample02.cpp693行目からの InitRaytracePipeline() が対象の命令です。

前回はルートシグネチャをグローバルとローカルの2つ作成しましたが、今回はローカルルートシグネチャを2つ作成しています。

グローバルルートシグネチャには cbInstance 以外のシェーダリソースを登録しています。

ローカルルートシグネチャの0番目はなにもシェーダリソースを登録していません。

このルートシグネチャはレイ生成シェーダとミスシェーダのためのもので、この2つのシェーダはグローバルルートシグネチャに設定したシェーダリソースのみ使用します。

ローカルルートシグネチャの1番目は HitGroup 用です。

こちらはインスタンスごとの定数バッファである cbInstance が登録されています。

登録する際には 32bit定数バッファを利用します。

現在の Fallback Layer はデフォルトでは32bit定数バッファか Descriptor Table がサポートされています。

ただ、Descriptor Table を試してみたところクラッシュしてしまったので、今回は32bit定数バッファを使用しています。

なお、MSがアップしているDXRのサンプルでは Descriptor Table が正常動作しています。

次の違いは HitGroup サブオブジェクトでしょう。

前回は1つだけ登録していましたが、今回は2つの HitGroup サブオブジェクトを登録しています。

片方は Lambertライティングの closest hit shader、もう片方は Half Lambert の closest hit shader です。

ローカルルートシグネチャとシェーダのバインド処理にも変化があります。

ローカルルートシグネチャの0番目は ray generate shader、miss shader とバインド、1番目は HitGroup0/1 とバインドします。

最後に重要なのがレイトレースコンフィグサブオブジェクトです。

ここで設定する MaxTraceRecursionDepth パラメータはレイの反射回数(深度)を指定する数値です。

今回は二次反射も求めるので 2 を指定しています。

…しかし、1 にしてもちゃんと二次反射が行われているので、実は意味を勘違いしてるかもしれません。

この関数の最後で cbScene に使用する定数バッファを作成していますが、D3D12 の普通の作り方なので割愛します。

今回はここまで

長くなりそうなので、残りは次回!