TOP > PROGRAM

シェーダテーブル

1 はじめに

きちんとしたレイトレを書く前に,マテリアルごとに別シェーダやら別テクスチャやらを切り替えられるようにする方法を知らないとな~…と思ったので,シェーダテーブルをちょっと使って見ました。下記の画像は1つのBLASをインスタンス化して,2つ配置しておりそれぞれ別のテクスチャと別のシェーダで処理した結果になっています。

図 1: シェーダテーブル

2 シェーダテーブル

まず,下図のように2つのポリゴンを画面上に描画する際に,異なるテクスチャと異なるシェーダを割り当てて描画したいというのが,今回の目的です。

図 2: やりたいこと

全部テクスチャをグローバルルートシグニチャに突っ込むという方法もありますが,今回は局所的に変えたいです。具体的にはマテリアルごとに変えたいので,ローカルルートシグニチャにテクスチャを設定し,シェーダレコードを複数作っておき,それらをシェーダテーブルに設定して,ヒットごとにマテリアルに対応するシェーダレコードを取得できるように,BLASの設定をしておきます。
 まず,シェーダレコードですが,これは実際に起動するシェーダを識別するデータと,そのシェーダで使用するパラメータやテクスチャなどをセットにしたものです。

図 3: シェーダレコード

ローカルルート引数というのは,ローカルルートシグニチャとして設定したパラメータそれぞれに対応します。前回やってわかるようにローカルルートシグニチャはステートオブジェクトごとに1つしか設定できません。つまり,シェーダレコードごとに別のルートシグニチャを設定できないという点に注意してください。次にシェーダテーブルですが,これはシェーダレコードを複数まとめたものとなります。

図 4: シェーダテーブル

 では,実際にプログラム上ではどのように生成して,設定していけばよいのか見ていくことにしましょう。まずは,前回やったシェーダをプログラムを改変して,シェーダレコードとして設定するシェーダを用意します。

//-----------------------------------------------------------------------------
//      交差後の処理です.
//-----------------------------------------------------------------------------
[shader("closesthit")]
void OnClosestHit1(inout RayPayload payload, in HitArgs args)
{
    // 重心座標を求める.
    float3 barycentrics = float3(
        1.0f - args.barycentrics.x - args.barycentrics.y,
        args.barycentrics.x,
        args.barycentrics.y);

    // プリミティブ番号取得.
    uint triangleIndex = PrimitiveIndex();

    // 頂点データ取得.
    Vertex v = GetVertex(triangleIndex, barycentrics);

    // 頂点カラー乗算して,色を求める.
    payload.Color = BaseColor.SampleLevel(LinearWrap, v.TexCoord, 0.0f) * v.Color;
}

//-----------------------------------------------------------------------------
//      交差後の処理です.
//-----------------------------------------------------------------------------
[shader("closesthit")]
void OnClosestHit2(inout RayPayload payload, in HitArgs args)
{
    // 重心座標を求める.
    float3 barycentrics = float3(
        1.0f - args.barycentrics.x - args.barycentrics.y,
        args.barycentrics.x,
        args.barycentrics.y);

    // プリミティブ番号取得.
    uint triangleIndex = PrimitiveIndex();

    // 頂点データ取得.
    Vertex v = GetVertex(triangleIndex, barycentrics);

    // テクスチャカラーのみ.
    payload.Color = BaseColor.SampleLevel(LinearWrap, v.TexCoord, 0.0f);
}

前回は,closesthitシェーダが1つでしたが,今回はマテリアルごとに分けるという想定で2つ用意しました。OnClosestHit1が頂点カラーにテクスチャカラーを乗算する形になっています。OnClosestHit2の方はテクスチャカラーのみを返す実装となっています。テクスチャ自体は,下記のように宣言してあります。

Texture2D BaseColor : register(t4);

 前回と異なるのは,テクスチャを扱えるように頂点データにテクスチャ座標データが追加されています。

///////////////////////////////////////////////////////////////////////////////
// Vertex structure
///////////////////////////////////////////////////////////////////////////////
struct Vertex
{
    float3 Position;    // 位置座標.
    float2 TexCoord;    // テクスチャ座標.
    float4 Color;       // 頂点カラー.
};

 データが追加されているので,GetVertex()メソッドも追加修正しています。

//-----------------------------------------------------------------------------
//      重心座標で補間した頂点データを取得します.
//-----------------------------------------------------------------------------
Vertex GetVertex(uint triangleIndex, float3 barycentrices)
{
    uint3 indices = GetIndices(triangleIndex);
    Vertex v = (Vertex)0;

    [unroll]
    for(uint i=0; i<3; ++i)
    {
        uint address = indices[i] * VERTEX_STRIDE;
        v.Position += asfloat(Vertices.Load3(address)) * barycentrices[i];

        address += TEXCOORD_OFFSET;
        v.TexCoord += asfloat(Vertices.Load2(address)) * barycentrices[i];

        address += COLOR_OFFSET;
        v.Color += asfloat(Vertices.Load4(address)) * barycentrices[i];
    }

    return v;
}

 これで,シェーダが準備できました。あとは,DXRが起動してヒット後に適切なシェーダが起動するように設定してあげるだけです。
今回はポリゴンが2つあり,それぞれ異なるシェーダとして起動するためにはどうすれば良いでしょうか?シェーダレコードはどのように選択されるのでしょうか?MicrosoftのDirectX-Specsにシェーダレコードの選択ルールが記述されています。仕様書によると次のようにヒットグループテーブルの番号付けが決定されるそうです。

図 5: ヒットグループテーブルの番号付け
※図は[Microsoft 2021]より引用

 上記をみると,GeometryContributionToHitGroupIndexというのがあるのですが,これはTLAS生成時に設定できるインデックスです。今回のサンプルではこれを使って,どのシェーダを起動するかの指定を行うことにします。具体的には次のようなコードになります。

    // 上位レベル高速化機構の生成.
    {
        asdx::DXR_INSTANCE_DESC desc[2] = {};
        {
            auto transform = asdx::FromMatrix(asdx::Matrix::CreateTranslation(asdx::Vector3(-1.0f, 0.0f, 0.0f)));
    
            memcpy(desc[0].Transform, &transform, sizeof(transform));
            desc[0].InstanceID             = 0;
            desc[0].InstanceMask           = 0xFF;
            desc[0].AccelerationStructure  = m_BLAS.GetResource()->GetGPUVirtualAddress();
            desc[0].InstanceContributionToHitGroupIndex = 0;
        }

        {
            auto transform = asdx::FromMatrix(asdx::Matrix::CreateTranslation(asdx::Vector3(1.0f, 0.0f, 0.0f)));

            memcpy(desc[1].Transform, &transform, sizeof(transform));
            desc[1].InstanceID              = 1;
            desc[1].InstanceMask            = 0xFF;
            desc[1].AccelerationStructure   = m_BLAS.GetResource()->GetGPUVirtualAddress();
            desc[1].InstanceContributionToHitGroupIndex = 1;
        }

        if (!m_TLAS.Init(pDevice, 2, desc, asdx::DXR_BUILD_FLAG_PREFER_FAST_TRACE))
        {
            ELOGA("Error : Tlas::Init() Failed.");
            return false;
        }
    }

 上記コードの,InstanceContributionToHitGroupIndexというものがそうです。desc[0]の方は,ヒットにした時にヒットグループ0番使ってね,desc[1]の方は,ヒットグループ1番使ってねと指定しています。これで,ジオメトリと起動するシェーダレコード番号の対応付けができました。あと残りは,適切にシェーダレコードとシェーダテーブルの設定をするだけです。
 まずは,シェーダレコードとして設定するシェーダをステートオブジェクトから取得できるようにします。先ほどコーディングした,OnClosestHit1とOnClosestHit2をステートオブジェクト生成時に設定するようにしておきます。次のような感じです。

    // ステートオブジェクトの生成.
    {
        asdx::SubObjects subObjects;

        D3D12_EXPORT_DESC exports[4] = {
            { L"OnGenerateRay", nullptr, D3D12_EXPORT_FLAG_NONE },
            { L"OnClosestHit1", nullptr, D3D12_EXPORT_FLAG_NONE },
            { L"OnClosestHit2", nullptr, D3D12_EXPORT_FLAG_NONE },
            { L"OnMiss",        nullptr, D3D12_EXPORT_FLAG_NONE },
        };

        // グローバルルートシグニチャの設定.
        D3D12_GLOBAL_ROOT_SIGNATURE globalRootSig = {};
        globalRootSig.pGlobalRootSignature = m_GlobalRootSig.GetPtr();
        subObjects.Push(&globalRootSig);

        // ローカルルートシグニチャの設定.
        D3D12_LOCAL_ROOT_SIGNATURE localRootSig = {};
        localRootSig.pLocalRootSignature = m_LocalRootSig.GetPtr();
        subObjects.Push(&localRootSig);

        // DXILライブラリの設定.
        D3D12_DXIL_LIBRARY_DESC dxilLib = {};
        dxilLib.DXILLibrary = { SampleRT, sizeof(SampleRT) };
        dxilLib.NumExports  = _countof(exports);
        dxilLib.pExports    = exports;
        subObjects.Push(&dxilLib);

        // ヒットグループ0の設定.
        D3D12_HIT_GROUP_DESC hitGroup = {};
        hitGroup.ClosestHitShaderImport = L"OnClosestHit1";
        hitGroup.HitGroupExport         = L"HitGroup1";
        hitGroup.Type                   = D3D12_HIT_GROUP_TYPE_TRIANGLES;
        subObjects.Push(&hitGroup);

        // ヒットグループ1の設定.
        D3D12_HIT_GROUP_DESC hitGroup2 = {};
        hitGroup2.ClosestHitShaderImport = L"OnClosestHit2";
        hitGroup2.HitGroupExport         = L"HitGroup2";
        hitGroup2.Type                   = D3D12_HIT_GROUP_TYPE_TRIANGLES;
        subObjects.Push(&hitGroup2);

        // シェーダ設定.
        D3D12_RAYTRACING_SHADER_CONFIG shaderConfig = {};
        shaderConfig.MaxPayloadSizeInBytes   = sizeof(asdx::Vector4) + sizeof(asdx::Vector3);
        shaderConfig.MaxAttributeSizeInBytes = sizeof(asdx::Vector2);
        subObjects.Push(&shaderConfig);

        // パイプライン設定.
        D3D12_RAYTRACING_PIPELINE_CONFIG pipelineConfig = {};
        pipelineConfig.MaxTraceRecursionDepth = 1;
        subObjects.Push(&pipelineConfig);

        // ステート設定取得.
        auto desc = subObjects.GetDesc();

        // ステートオブジェクトを生成.
        auto hr = pDevice->CreateStateObject(&desc, IID_PPV_ARGS(m_StateObject.GetAddress()));
        if (FAILED(hr))
        {
            ELOGA("Error : ID3D12Device::CreateStateObject() Failed. errcode = 0x%x", hr);
            return false;
        }
    }

 335行目で設定しているローカルルートシグニチャは,今回テクスチャだけ指定して生成しています。

    // ローカルルートシグニチャの生成. 
    // シェーダテーブルごとに異なるものをはこちらで設定する.
    {
        asdx::RootSignatureDesc desc;
        desc.SetFlag(D3D12_ROOT_SIGNATURE_FLAG_LOCAL_ROOT_SIGNATURE);
        desc.AddTable("BaseColor", asdx::SV_ALL, D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 4, 0);

        if (!m_LocalRootSig.Init(pDevice, desc))
        {
            ELOGA("Error : RootSignature::Init() Failed.");
            return false;
        }
    }

 ステートオブジェクトが生成できたら,あとはID3D12StateObjectProperties::GetShaderIndentifier()メソッドを使って,シェーダレコードの設定とシェーダテーブルの生成を行います。

    // シェーダテーブルの生成.
    {
        asdx::RefPtr<ID3D12StateObjectProperties> props;
        auto hr = m_StateObject->QueryInterface(IID_PPV_ARGS(props.GetAddress()));
        if (FAILED(hr))
        {
            ELOGA("Error : ID3D12StateObject::QueryInteface() Failed. errcode = 0x%x", hr);
            return false;
        }

        // レイ生成シェーダテーブル.
        {
            asdx::ShaderRecord record;
            record.ShaderIdentifier = props->GetShaderIdentifier(L"OnGenerateRay");

            if (!m_RayGenTable.Init(pDevice, 1, &record))
            {
                ELOGA("Error : ShaderTable::Init() Failed.");
                return false;
            }
        }

        // ミスシェーダテーブル.
        {
            asdx::ShaderRecord record;
            record.ShaderIdentifier = props->GetShaderIdentifier(L"OnMiss");

            if (!m_MissTable.Init(pDevice, 1, &record))
            {
                ELOGA("Error : ShaderTable::Init() Failed.");
                return false;
            }
        }

        // ヒットグループシェーダテーブル
        {
            const auto kGeometryCount = 2;

            struct LocalParam
            {
                D3D12_GPU_DESCRIPTOR_HANDLE Handle;
            } param[kGeometryCount];

            for(auto i=0; i<kGeometryCount; ++i)
            { param[i].Handle = m_BaseColor[i].GetView()->GetHandleGPU(); }

            // シェーダとテクスチャをそれぞれ別割り当てする.
            asdx::ShaderRecord record[kGeometryCount];

            record[0].ShaderIdentifier   = props->GetShaderIdentifier(L"HitGroup1");
            record[0].LocalRootArguments = &param[0];

            record[1].ShaderIdentifier   = props->GetShaderIdentifier(L"HitGroup2");
            record[1].LocalRootArguments = &param[1];

            if (!m_HitGroupTable.Init(pDevice, _countof(record), record, sizeof(LocalParam)))
            {
                ELOGA("Error : ShaderTable::Init() Failed.");
                return false;
            }
        }
    }

 上記のコードで大事なところは,ヒットグループシェーダテーブルを生成している箇所です。LocalRootArgumentsの部分にはローカルルートシグニチャの定義と会うようにディスクリプタハンドルを設定します。今回はテクスチャ1つだけですので,ディスクリプタハンドルを1つだけ設定しています。ポリゴン2つそれぞれ別のもの設定したいので,kGeometryCount = 2としています。これで,シェーダレコードとシェーダテーブルが適切に設定できました。あとは,DispatchRays()を叩いて,DXRを実行してみましょう。実行結果の例は次のようになります。

図 6: 実行結果

3 おわりに

 今回は,しょぼいプログラムでしたが,ジオメトリのインスタンシングとシェーダテーブルを紹介し,マテリアルの仕様を想定して異なるシェーダと異なるテクスチャを割り当てる方法を学びました。次回は,パストレやりたいなと思います。

4 参考文献

サンプルコード

本ソースコードおよびプログラムはMIT Licenseに準じます。 プログラムの作成にはMicrosoft Visual Studio Community 2019, 及び Windows SDK 10.0.20161.0を用いています。