TOP > PROGRAM

Ray Query

1 はじめに

最近、今更ながらレイトレーシング周りの各プラットフォームのラッパーAPIを仕事で書いていて,DXR 1.0的な仕様にしてみて「出来た!」と思って、実際にサンプルを書き始めたら,これがもうどうしようもないほど使い物にならんなぁー…というものになってしまいました。
 そこで,「他社さんはどうしているんだろ?」って調べてみたら,昨年のCAPCOM Open Conference Professional RE:2023で,Inline Ray Tracingを利用しているとの発表があって,「やっぱりそうだよねー」と感じましたし,昨年のCEDECのブースではVulkanでInline Ray Tracingしかサポートしないと明言していた特定ハードウェアメーカーさんもいたので,どのプラットフォームでもレイトレ対応させようとなると,やっぱりInline Ray Tracingベースの方が絶対にいいよなぁ…と思ったので,今回はInline Ray Tracingを実現するためのRay Queryについて取り扱って見ます。

Ray Query
図 1: Ray Query

2 Ray Queryとは?

 RayQueryとは,Direct RayTracing(DXR) Tier 1.1から導入されたInline Ray Tracingを実現するための機能です。ここでいうInline Ray TracingはDXR1.0のようにレイトレーシングパイプラインで使うような特定のシェーダに依存せずに,ピクセルシェーダやコンピュートシェーダなどあらゆるシェーダステージからレイトレーシングを実行できる仕組みのことを指します。
 Inline Ray Tracingは前述したように,DXR Tier 1.1の機能ですので,プログラムで使用する場合はTier 1.1がサポートされているかどうかをチェックする必要があります。C++側のチェック処理は次のようになります。

    // Ray Tracing Tier 1.1 がサポートされているかどうかチェック.
    {
        D3D12_FEATURE_DATA_D3D12_OPTIONS5 options = {};
        auto hr = pDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &options, sizeof(options));
        if (FAILED(hr))
        { return false; }

        // サポートされていなければ終了.
        if (options.RaytracingTier < D3D12_RAYTRACING_TIER_1_1)
        {
            ELOGA("Error : RayTracing Tier 1.1 is not supported.");
            return false;
        }
    }

D3D12_FEATURE_DATA_D3D12_OPTIONS5 をID3D12Device::CheckFeatureSupport()メソッドを介して問い合わせし,RaytracingTierの値がD3D12_RAYTRACING_TIER_1_1以上であればTier 1.1がサポートされていることになります。
 あとは,普通のDXRのプログラムと同じように,AccelerationStructureを作成して,シェーダ上で参照できるように渡します。ピクセルシェーダでもRayQueryは使えるのですが,今回のサンプルではコンピュートシェーダでRayQueryを実行するようにしてみました。ちなみにRayQueryを使用するためには,Shader Model 6.5以上が必要になるので注意してください。まず,コンピュートシェーダの実装ですが,下記のようになります。

//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//-----------------------------------------------------------------------------
[numthreads(8, 8, 1)]
void main
(
    uint3 dispatchId : SV_DispatchThreadID,
    uint  groupIndex : SV_GroupIndex
)
{
    uint2 remappedId = RemapLane8x8(dispatchId.xy, groupIndex);
    if (any(remappedId >= SceneBuffer.TargetSize)) 
    { return; }

    float2 index = (float2) remappedId * SceneBuffer.InvTargetSize;

    // レイを求める.
    float3 rayOrigin;
    float3 rayDirection;
    CalcRay(index, rayOrigin, rayDirection);

    // Let's レイトレ!
    HitRecord hit = Intersect(
        SceneTlas,
        RAY_FLAG_NONE,
        ~0,
        rayOrigin,
        rayDirection,
        1e-3f,
        10000.0f);

    float4 color;

    // 交差した場合.
    if (hit.IsHit())
    {
        Vertex v = GetVertex(hit.PrimitiveIndex, hit.GetBaryCentrics());
        color = v.Color;
    }
    else
    {
        float2 uv = ToSphereMapCoord(rayDirection);
        color = Background.SampleLevel(LinearWrap, uv, 0.0f);
    }

    Canvas[remappedId] = color;
}

画面上の座標を求めて,CalcRay()で飛ばすレイを求めます。このメソッドの定義は下記のように,w=1に射影した位置を求めて,カメラ位置と射影位置を結ぶ方向ベクトルを求めてレイを発射する方向とします。レイを発射する原点はカメラ位置となりますので,ビュー空間上のカメラ位置(0.0, 0.0, 0.0)に対して,ビュー行列の逆行列を掛けてカメラ位置を求めます。パフォーマンスを考えるのであれば,行列計算は重いので,直接カメラ位置とカメラの基底ベクトルを使って計算する方が一般的かと思います。
 続いて,Intersect()メソッドを用いて交差判定を行っています。この関数はRayQueryを使ったラッパー関数となっており,実装は下記のようにしています。

// 無効なインデックス.
#define INVALID_INDEX 0xFFFFFFFF

// TLASの定義.
typedef RaytracingAccelerationStructure Tlas;

///////////////////////////////////////////////////////////////////////////////
// HitRecord structure
///////////////////////////////////////////////////////////////////////////////
struct HitRecord
{
    uint    InstanceIndex;      //!< インスタンス番号.
    uint    PrimitiveIndex;     //!< プリミティブ番号.
    float2  BaryCentrics;       //!< 重心座標.
    float   Distance;           //!< ヒット距離.
    uint    Status;             //!< コミットされた状態.

    bool IsHit()
    { return Status != COMMITTED_NOTHING; }

    float3 GetBaryCentrics()
    { return float3(BaryCentrics, saturate(1.0f - BaryCentrics.x - BaryCentrics.y)); }
};

//-----------------------------------------------------------------------------
//      交差判定を行います.
//-----------------------------------------------------------------------------
HitRecord Intersect
(
    Tlas    tlas,                   //!< 高速化機構.
    uint    rayFlags,               //!< レイフラグ.
    uint    instanceInclusionMask,  //!< インスタンスマスク.
    float3  rayOrigin,              //!< レイの原点.
    float3  rayDirection,           //!< レイの方向ベクトル.
    float   tMin,                   //!< 交差許容最小距離.
    float   tMax                    //!< 交差許容最大距離.
)
{
    // レイの設定.
    RayDesc rayDesc;
    rayDesc.Origin    = rayOrigin;
    rayDesc.Direction = rayDirection;
    rayDesc.TMin      = tMin;
    rayDesc.TMax      = tMax;

    RayQuery<RAY_FLAG_NONE> rayQuery;

    // レイトレ実行.
    rayQuery.TraceRayInline(
        tlas,
        rayFlags,
        instanceInclusionMask,
        rayDesc);
 
    // 交差確定するまでループ.
    while (rayQuery.Proceed())
    {
        switch(rayQuery.CandidateType())
        {
        case CANDIDATE_NON_OPAQUE_TRIANGLE:
            { rayQuery.CommitNonOpaqueTriangleHit(); }
            break;

        case CANDIDATE_PROCEDURAL_PRIMITIVE:
            { rayQuery.CommitProceduralPrimitiveHit(rayQuery.CommittedRayT()); }
            break;
        }
    }

    const bool isHit = rayQuery.CommittedStatus() != COMMITTED_NOTHING;

    HitRecord result;
    result.PrimitiveIndex = (isHit) ? rayQuery.CommittedPrimitiveIndex() : INVALID_INDEX;
    result.InstanceIndex  = (isHit) ? rayQuery.CommittedInstanceIndex () : INVALID_INDEX;
    result.BaryCentrics   = (isHit) ? rayQuery.CommittedTriangleBarycentrics() : 0.0f.xx;
    result.Distance       = (isHit) ? rayQuery.CommittedRayT() : -1.0f;
    result.Status         = rayQuery.CommittedStatus();

    return result;
}

最初に第2引数をRAY_FLAG_NONEにして実装した際に,144行目から153行目の部分の実装は無かったのですが,この場合だと衝突判定されませんでした。RAY_FLAG_FORCE_OPAQUEを第2引数に渡すと交差するようになったので,DirectX-Specsを見ていたのですが,どうも指定するRayFlagsによってかなり挙動が変わるようです。
 RayQueryのテンプレート引数をRAY_FLAG_NONEにした場合の挙動は下図のようになるようです。

RAY_FLAG_NONEを指定場合の挙動

※図は[Microsoft 2023]より引用.

一方,RAY_FLAG_CULL_NON_OPAQUE | RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH を指定した場合は,次のようになるようです。

RAY_FLAG_CULL_NON_OPAQUE | RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES | RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH を指定した場合の挙動

※図は[Microsoft 2023]より引用.

DirectX-Specsによると後者の設定の方が,パフォーマンスの高いインラインレイトレーシングコードを生成できるようになるそうです。確かにフローチャートの図から見てもそれは理解できますね。
 さて,RayQueryですが,上記のコードを見ると分かるようにテンプレート引数を必要とします。テンプレート引数の型はRAY_FLAGS列挙型であり,値は次の通りです。

意味
RAY_FLAG_NONE オプション無し.
RAY_FLAG_FORCE_OPAQUE 全てのプリミティブを不透明として交差を取り扱います。インスタンスフラグに関係なくAnyHitシェーダは実行されません。
RAY_FLAG_FORCE_NON_OPAQUE 全てのプリミティブを透明・半透明として交差を取り扱います。インスタンスフラグに関係なくAnyHitシェーダが実行されます。
RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH レイトレースで最初にレイとプリミティブの交差が発生すると,Any Hitシェーダの直後にAcceptHitAndEndSearch()が自動的に呼び出されます。
RAY_FLAG_SKIP_CLOSEST_HIT_SHADER ClosestHitシェーダをスキップします。
RAY_FLAG_CULL_BACK_FACING_TRIANGLES 背面カリングを有効にします。
RAY_FLAG_CULL_FRONT_FACING_TRIANGLES 前面カリングを有効にします。
RAY_FLAG_CULL_OPAQUE 不透明とみなされるすべてのプリミティブをカリングします。
RAY_FLAG_CULL_NON_OPAQUE 透明・半透明とみなされるすべてのプリミティブをカリングします。
RAY_FLAG_SKIP_TRIANGLES すべての三角形プリミティブをカリングします。
RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVE すべてのプロシージャルプリミティブをカリングします。

RayQueryオブジェクトを定義したら,TraceRayInline()を使うことでインラインレイトレーシングが実行できます。この関数の定義は次の通りです。

void RayQuery::TraceRayInline(
    RaytracingAccelerationStructure accelerationStructure,
    uint    rayFlags,
    uint    instanceInclusionMask,
    RayDesc ray
);

第1引数には,Top-Level Acceleration Structureを指定します。NULLを指定した場合は強制的にミスとなります。第2引数にはRAY_FLAGSを指定します。これは,テンプレート引数で指定したRAY_FLAGSの値とこの第2引数のRAY_FLAGSの値がOR演算されたものがレイトレーシングする際に指定されます。第3引数のinstanceInclusionMaskの下位8ビットは各インスタンスのInstanceMaskにもどついて,ジオメトリを含めるか棄却するか判定するために使用するために使用されます。第4引数のrayは発射するレイを指定します。この関数を呼び出すことでインラインレイトレーシングの実行はできるのですが,レイトレーシングの完了は保証されていません。そこで,上述したようにフローチャートの図のようにProceed()を呼び出すことで,RayQueryを続行するかどうかを問い合わせます。falseが返却された場合は,交差候補の検索が終了したことを意味します。つまり,交差が確定したということですね。trueが返却された場合は,交差の候補があることを示し,RayQuery::CandidateType()を呼び出すことで,候補タイプが分かります。あとは,その候補タイプに応じて処理を行います。DirectX-Specsに載っている疑似コードは次のような感じです。

RaytracingAccelerationStructure myAccelerationStructure : register(t3);

struct MyCustomAttrIntersectionAttributes { float4 a; float3 b; }

[numthreads(64,1,1)]
void MyComputeShader(uint3 DTid : SV_DispatchThreadID)
{
    ...
    // Instantiate ray query object.
    // Template parameter allows driver to generate a specialized
    // implemenation.  No specialization in this example.
    RayQuery<RAY_FLAG_NONE> q;

    // Set up a trace
    q.TraceRayInline(
        myAccelerationStructure,
        myRayFlags,
        myInstanceMask,
        myRay);

    // Storage for procedural primitive hit attributes
    MyCustomIntersectionAttributes committedCustomAttribs;

    // Proceed() is where behind-the-scenes traversal happens,
    // including the heaviest of any driver inlined code.
    // Returns TRUE if there's a task for the shader to perform
    // as part of traversal
    while(q.Proceed())
    {
        switch(q.CandidateType())
        {
        case CANDIDATE_PROCEDURAL_PRIMITIVE:
        {
            float tHit;
            MyCustomIntersectionAttributes candidateAttribs;

            // For procedural primitives, opacity is handled manually -
            // if an intersection is determined to not be opaque, just don't consider it
            // as a candidate.
            while(MyProceduralIntersectionEnumerator(
                tHit,
                candidateAttribs,
                q.CandidateInstanceIndex(),
                q.CandidatePrimitiveIndex(),
                q.CandidateGeometryIndex()))
            {
                if( (q.RayTMin() <= tHit) && (tHit <= q.CommittedRayT()) )
                {
                    if(q.CandidateProceduralPrimitiveNonOpaque() &&
                        !MyProceduralAlphaTestLogic(
                            tHit,
                            attribs,
                            q.CandidateInstanceIndex(),
                            q.CandidatePrimitiveIndex(),
                            q.CandidateGeometryIndex()))
                    {
                        continue; // non opaque
                    }

                    q.CommitProceduralPrimitiveHit(tHit);
                    committedCustomAttribs = candidateAttribs;
                }
            }
            break;
        }
        case CANDIDATE_NON_OPAQUE_TRIANGLE:
        {
            if( MyAlphaTestLogic(
                q.CandidateInstanceIndex(),
                q.CandidatePrimitiveIndex(),
                q.CandidateGeometryIndex(),
                q.CandidateTriangleRayT(),
                q.CandidateTriangleBarycentrics(),
                q.CandidateTriangleFrontFace() )
            {
                q.CommitNonOpaqueTriangleHit();
            }
            if(MyLogicSaysStopSearchingForSomeReason()) // not typically applicable
            {
                q.Abort(); // Stop traversing and next call to Proceed()
                           // will return FALSE.
                           // Post-traversal results will just be based
                           // on what has been encountered so far.
            }
            break;
        }
        }
    }
    switch(q.CommittedStatus())
    {
    case COMMITTED_TRIANGLE_HIT:
    {
        // Do hit shading
        ShadeMyTriangleHit(
            q.CommittedInstanceIndex(),
            q.CommittedPrimitiveIndex(),
            q.CommittedGeometryIndex(),
            q.CommittedRayT(),
            q.CommittedTriangleBarycentrics(),
            q.CommittedTriangleFrontFace() );
        break;
    }
    case COMMITTED_PROCEDURAL_PRIMITIVE_HIT:
    {
        // Do hit shading for procedural hit,
        // using manually saved hit attributes (customAttribs)
        ShadeMyProceduralPrimitiveHit(
            committedCustomAttribs,
            q.CommittedInstanceIndex(),
            q.CommittedPrimitiveIndex(),
            q.CommittedGeometryIndex(),
            q.CommittedRayT());
        break;
    }
    case COMMITTED_NOTHING:
    {
        // Do miss shading
        MyMissColorCalculation(
            q.WorldRayOrigin(),
            q.WorldRayDirection());
        break;
    }
    }
    ...
}

 今回のサンプルではこのような判定ルーチンを関数内部に組み込みせず,使う側に判断をゆだねる実装にしたいため,候補データをそのままHitRecordとして返却し,その値からユーザー側であれこれ出来るような実装にしたかったため,Intersect()メソッド内で,CommitNonOpaqueTriangleHit() あるいは,CommitProceduralPrimitiveHit() を呼び出し,候補をコミットしてしまう実装としました。
 今回の実装では使っていませんが,RayQueryを強制的に終了にしたい場合は,RayQuery::Abort()を呼び出しすることでトレースを終了することが出来ます。
 RayQuery::Proceed()のループを抜けると,交差状態が確定しています。RayQuery::CommittedStatus()を呼び出し,返却値が COMMITTED_NOTHING であれば交差が無かったことが分かります。COMMITTED_TRIANGLE_HITであれば三角形プリミティブと交差があり,COMMITTED_PROCEDURAL_PRIMITIVE_HITであれば,プロシージャルプリミティブとの交差があったことがわかります。交差したデータを得るためには,ComittedXXXというような関数を呼び出してあげればよいです。呼び出し可能な関数の例は下記の通りです。

uint CommiittedRayT();
uint CommittedInstanceID();
uint CommittedInstanceContributionToHitGroupIndex();
uint CommittedGeometryIndex();
uint CommittedPrimitiveIndex();
float3 CommittedObjectRayOrigin();
float3 CommittedObjectRayDirection();
float3x4 CommittedObjectToWorld3x4();
float4x3 CommittedObjectToWorld4x3();
float3x4 CommittedWorldToObject3x4();
float4x3 CommittedWorldToObject4x3();
float2 CommittedTriangleBarycentrics();
bool CommittedTriangleFrontFace();

上記の各関数はCommittedStatus()の値によって呼び出し可能か否かが規定されているので,詳細についてはDirectX-Specsを参照してください。大抵の場合は,Procedural Primitiveを使うのは特殊ケースかと思いますので,三角形プリミティブに対応できるようにこのサンプルでは,重心座標とレイの交差距離,プリミティブ番号,インスタンス番号,ヒットタイプを返却するようにしています。
 これで,交差データまでが取得できたので,あとはユーザー側で交差に応じてやりたい処理を実行します。今回は,交差があった場合は,重心座標で補間された頂点カラーを出力し,そうでない場合は,環境マップのテクセルを返却するように実装しました。
 実行すると下記のような描画結果が得られるかと思います。

実行結果

3 おわりに

 今回は,Inline RayTracingを実現するためのRayQueryについて紹介しました。
恐らく,使い勝手とか実装を考えるとDXR 1.0的なものは流行らない気がするので,RayQueryだけ対応しておけば,大抵の用途には対応できそうな気がします。…というのもReSTIRなどの登場もありますが,まだまだリアルタイムでパストレするのはゲームには厳しい気がしますね。これはAPIの使い勝手の悪さや,実用速度的に厳しいものがあります。しかし,現時点で厳しいからと言って,ベイク用途等を考えれば大幅高速化も見込めますので,全捨てするものでは決してないかと思います。また,研究動向を見ているとかなり進歩も活発な分野であるように見えますので,この分野に関して技術的な投資をすべきかと個人的には思います。知らんけど。

4 参考文献

5 サンプルコード.

 本ソースコードおよびプログラムはMIT Licenseに準じます。 プログラムの作成にはMicrosoft Visual Studio Community 2022, 及び DirectX Agility SDK 1.613.0, DirectX Shader Compiler 1.8.2403.18 を使用しています。
 動作確認環境は,Windows 10 Home 22H2, NVIDIA GeForce RTX 2070 , Driver Version 551.86 です。