1. サイトトップ
  2. ブログ
  3. C++
  4. Amplification Shaderを用いたカリング

Amplification Shaderを用いたカリング

こんにちは。ロジカルビート情熱開発部プログラム課・廣江です。

今回はAmplification ShaderでMeshlet単位のカリングを行ってみようと思います。
前回の私の担当したブログではMesh Shaderについて紹介しました。本記事はMesh Shaderの記事の内容を前提としている部分もあるため、あらかじめご了承ください。

今回の検証は以下の環境で行いました。

それでは初めにカリングとAmplification Shaderについて簡単に説明します。

カリングとは

ではカリングから簡単に説明します。

カリングとは、最終的な描画結果に影響しないポリゴンを描画しないようにする手法です。
有名なものだと、背面カリング、視錐台カリング、オクルージョンカリングなどがあります。

背面カリング

背面カリングは、ポリゴンの表裏を判定し、描画の有無を判定する処理です。
ポリゴンの表裏は法線によって決定されます。

視錐台カリング

視錐台カリングとは、オブジェクトがカメラの視錐台の内にあるのか判定し、 描画の有無を判定する処理です。
次の画像のように円と四角のオブジェクトはカメラの視錐台の中にあるため、描画します。ですが、ひし形のオブジェクトはカメラの視錐台の外にあるため描画は行いません。

視錐台カリングのイメージ図

詳しくは弊社のブログに記事があるので、そちらをご覧ください。

オクルージョンカリング

オクルージョンカリングとは、不透明オブジェクトにより別オブジェクトがカメラから遮蔽される場合に描画しないようにする処理です。
次の画像のようにカメラから見た際、オブジェクトAはオブジェクトBにより遮蔽されているため、描画は行いません。

オクルージョンカリングのイメージ図

今回はこの中の視錐台カリングを使用します。

Amplification Shaderとは

次に今回使用するAmplification Shaderについて説明します。

Amplification Shaderとは、Mesh Shaderの前に実行され、内部でDispatchMeshを呼び出すことがでるシェーダです。
また、Amplification Shaderはオプションのため使用しない選択肢もあります。

次の画像は従来のジオメトリパイプラインと、Amplification Shader、Mesh Shaderを使用したジオメトリパイプラインの比較画像です。
Amplification Shaderを使用することで、カリング処理などをジオメトリパイプライン内に組み込むことが可能です。

ジオメトリパイプラインの比較

主な使用用途

Amplification Shaderは、Mesh Shaderと組み合わせMeshlet単位でのカリングやLODを行えます。
これにより描画前に処理する頂点数を効率的に減らすことができ、より大量の頂点を処理することができます。

今回はこのAmplification Shaderを使用してMeshlet単位でのカリングを行ってみようと思います。

Meshlet単位でのカリング

では本題のAmplification Shaderを使用したMeshlet単位でのカリングをやってみようと思います。

HLSLコード

まずは今回使用するAmplification Shaderのコードを一部紹介します。

// Resource
groupshared PAYLOAD_PARAM       s_Payload;
ConstantBuffer<CULL_COMMON>     g_cbCullCommon      : register(b11);
ConstantBuffer<MESHLET_INFO>    g_cbMeshletInfo     : register(b12);
StructuredBuffer<CULL_DATA>     g_CullData          : register(t14);

// 可視性チェック
bool IsVisible(CULL_DATA cullData)
{
    float4 center = float4(cullData.BoundingSphere.xyz, 1.0f);
    
    for (int i = 0; i < 6; i++)
    {
        if (dot(center, g_cbCullCommon.planes[i]) < -cullData.BoundingSphere.w)return false;
    }
    
    return true;
}

// Entry point.
[RootSignature(ROOT_SIG)]
[numthreads(32, 1, 1)]
void main( uint dtid : SV_DispatchThreadID )
{
    bool visible = false;

    if (dtid < g_cbMeshletInfo.MeshletCount)
    {
        //visible = true;
        visible = IsVisible(g_CullData[dtid]);
    }

    if (visible)
    {
        uint index = WavePrefixCountBits(visible);
        s_Payload.MeshletIndices[index] = dtid;
    }

    // Dispatch the required number of MS threadgroups to render the visible meshlets
    uint visibleCount = WaveActiveCountBits(visible);
    DispatchMesh(visibleCount, 1, 1, s_Payload);
}

見て頂いてわかる通り、Amplification ShaderはMesh ShaderやCompute Shaderと書き方が似ています。

では、コードの解説をしていきます。

まずは、カリングの計算です。

bool IsVisible(CULL_DATA cullData)
{
    float4 center = float4(cullData.BoundingSphere.xyz, 1.0f);
    
    for (int i = 0; i < 6; i++)
    {
        if (dot(center, g_cbCullCommon.planes[i]) < -cullData.BoundingSphere.w)return false;
    }
    
    return true;
}

IsVisible()で視錐台カリングの計算を行っています。入力値のCULL_DATAはMeshletのBoundingSphereなどが含まれています。CULL_DATAの生成方法については後ほど説明します。

uint index = WavePrefixCountBits(visible);

uint visibleCount = WaveActiveCountBits(visible);

次はWave Intrinsicsについてです。Wave IntrinsicsはHLSLのシェーダモデル6.0から導入された新しい組み込み関数群です。
Wave IntrinsicsはWaveと呼ばれる複数のスレッド間でのデータの交換や演算を行うための組み込み関数です。

WavePrefixCountBits()は自身のLane Index未満のActive Laneで、引数にtrueを指定した個数を返します。
つまり、何番目に成功したのかのインデックスを取得しています。

WaveActiveCountBits()は引数にtrueを指定したすべてのActive Laneの数を取得します。
つまり、可視性チェックを通ったMeshletの数を取得しています。

詳細に関しては説明すると長くなりますので、Wave Intrinsicsを解説している記事を紹介させていただきます。
Wave Intrinsicsの解説記事( https://shikihuiku.github.io/post/wave_intrinsics1/ )

DispatchMesh(visibleCount, 1, 1, s_Payload);

最後にDispatchMeshの呼び出しを見ていきます。
まず、visibleCountは視錐台カリングの判定を通ったMeshletの数です。
s_PayloadはMesh Shaderへ渡すことができる変数です。今回はこれに視錐台カリングの判定を通ったMeshletのインデックスを設定しています。

カリングデータの生成

次はカリングデータの生成方法について説明します。

Amplification Shaderでカリングを効率的に行うために、Meshlet単位でカリングデータを生成します。
データの構造体と生成関数はDirectXMathライブラリに用意されています。

次の構造体は生成するカリングデータの構造体です。

struct CullData
{
    DirectX::BoundingSphere             BoundingSphere; // xyz = center, w = radius
    DirectX::PackedVector::XMUBYTEN4    NormalCone;     // xyz = axis, w = -cos(a + 90)
    float                               ApexOffset;     // apex = center - axis * offset
};

カリングデータの生成は次の通りです。

// 生成済みデータ
std::vector<cVec3> vertPosTbl;
std::vector<DirectX::Meshlet> meshlets;
std::vector<uint8_t> uniqueVertIB;
std::vector<DirectX::MeshletTriangle> primIndices;
// 生成するCullData
std::vector<DirectX::CullData> cullDatas;

DirectX::ComputeCullData(
			vertPosTbl.data(), vertPosTbl.size(), 
			meshlets.data(), meshlets.size(),
			reinterpret_cast<u32*>(uniqueVertIB.data()), uniqueVertIB.size(),
			primIndices.data(), primIndices.size(),
			cullDatas.data() );

入力値は以前紹介したMeshletの生成方法をご確認ください。

DispatchMeshの呼び出し

では実際にDispatchMeshを呼び出し、モデルを描画してみます。

pCommandList->SetGraphicsRootShaderResourceView(0, pMeshlets->GetGPUVirtualAddress());
pCommandList->SetGraphicsRootShaderResourceView(1, pUniqueVertexIndices->GetGPUVirtualAddress());
pCommandList->SetGraphicsRootShaderResourceView(2, pPrimIndices->GetGPUVirtualAddress());
pCommandList->SetGraphicsRootShaderResourceView(3, pCullData->GetGPUVirtualAddress());

// DivRoundUp(MeshletCount, 32) → (MeshletCount + 32 - 1) / 32
pCommandList->DispatchMesh(DivRoundUp(MeshletCount, 32), 1, 1);

特に変わったことはしてませんが、一点だけDivRoundUp(MeshletCount, 32)について説明します。
DivRoundUp関数は、整数同士を除算し切り上げを行っています。
今回32でMeshletCountを割っているのは、使用するAmplification Shaderのスレッド数が32スレッドあるからです。

描画結果

オブジェクト単位とMeshlet単位のカリングの画像です。

オブジェクト単位のカリング
Meshlet単位のカリング

最後に

今回はAmplification ShaderでMeshlet単位のカリングを行ってみました。
従来のカリングだとオブジェクト単位でのカリングがほとんどだったかと思います。
ですが、Amplification ShaderとMesh Shaderの組み合わせで、より細かな単位でのカリングが可能となりました。
また今回はカリングのみでしたが、これにLODも組み合わせるとより高解像度のモデルをゲームで扱えるようになると思います。

簡単な紹介ではありましたが、皆さんのご参考になれば幸いです。

参考文献

以下のサイトを参考にさせていただきました。ありがとうございます。