TOP > PROGRAM

メッシュレットカリング

1 はじめに

メッシュレットカリングやってみたかったので,実装してみました。

メッシュレットカリング
図 1: メッシュレットカリング

2 モデルの準備

メッシュレットの生成方法などの説明は出版予定の本に書きましたので,そちら見てください。
発売日が発表されたらリンクを張っておきます。

3 Amplification Shaderの準備

Microsoftの公式サンプルに習って,視錐台カリングと法錐カリングをAmplificationシェーダで行います。
まず,視錐台カリングから。視錐台カリングはモデルを用意する際にメッシュレットごとのバウンディングスフィアを求めているので,バウンディングスフィアと錐台を構成する6平面との符号付き距離を求めて,内側にある場合はOK,外側にある場合はNGとしてカリングを行います。実装コードは次のようになります。

//-----------------------------------------------------------------------------
//      視錐台カリングを行います.
//-----------------------------------------------------------------------------
bool IsCull(float4 planes[6], float4 sphere)
{
    // sphereは事前に位置座標がワールド変換済み,半径もスケール適用済みとします.
    float4 center = float4(sphere.xyz, 1.0f);

    for(int i=0; i<6; ++i)
    {
        if (dot(center, planes[i]) < -sphere.w)
        { return true; }
    }

    // カリングしない.
    return false;
}

 視錐台カリングは,例えば[Lighthouse3d.com 2020]など,ちょっとググれば色々と情報が出てくるので調べてみてください。視錐台を構成する平面の求め方については,[Gribb 2001]に詳しく記載されており,これに従って実装を行っています。
次は,法錐カリングです。まず法錐とは何ぞ?というところですが,かなり大雑把にいうと下図のようにポリゴンの法線の集合を表すものです。

法錐
図 2: 法錐

基本的な考え方は,普通のバックフェースカリングと同じで,視線ベクトルの逆方向と法錐の軸ベクトルとの内積をみて,裏側になる場合にカリングを行います。通常のバックフェースカリングでは,90度以上かどうかになりますが,法錐の場合は図 3のように90度よりもちょっと進んだ所になります。

法錐カリング
図 3: 法錐カリング

法錐の内角を \(\alpha\)とすると…

\[\begin{eqnarray} \cos(\alpha + \frac{\pi}{2}) = -\sin(\alpha) \tag{1} \end{eqnarray}\]

となります。
 [Wihlidal 2016]のスライドに記載されている通りに実装すればよいことになります。

CulsterCulling
図 4: CulsterCulling

法錐の作成方法ですが,DirectX-Graphics-Samplesにサンプルコードが載っています([Microsoft 2020a]参照)。実装を見るとポリゴンの法線群を調べて,最大・最小となる法線をそれぞれ3つ見つけて,差分が最も大きくなるインデックスを求めます。あとは求めたインデックスを使って最大値と最小値を足して2で割り,法錐の軸を見つけているようです。法錐の内角についてですが,法錐の軸ベクトルと法線群の1つ1つの内積の最小値を地道に調べて求めているようです([Microsoft 2020b]参照)。
 さて、今回実装したコードは次のようになります。

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include "Math.hlsli"

// デバッグチェック用.
#define DEBUG_CULLING   (1)


///////////////////////////////////////////////////////////////////////////////
// MeshParam structure
///////////////////////////////////////////////////////////////////////////////
struct MeshParam
{
    float4x4    World;
    float       Scale;
    float3      Padding0;
};

///////////////////////////////////////////////////////////////////////////////
// SceneParam structure
///////////////////////////////////////////////////////////////////////////////
struct SceneParam
{
    float4x4    View;
    float4x4    Proj;
    float4      Planes[6];
    float3      CameraPos;
    float       Padding0;
    float3      DebugCameraPos;
    float       Padding1;
    float4      DebugPlanes[6];
};

///////////////////////////////////////////////////////////////////////////////
// CullInfo structure
///////////////////////////////////////////////////////////////////////////////
struct CullInfo
{
    float4 BoundingSphere;
    uint   NormalCone;
};

///////////////////////////////////////////////////////////////////////////////
// MeshletInfo structure
///////////////////////////////////////////////////////////////////////////////
struct MeshletInfo
{
    uint   MeshletCount;
};

///////////////////////////////////////////////////////////////////////////////
// PayloadParam structure
///////////////////////////////////////////////////////////////////////////////
struct PayloadParam
{
    uint MeshletIndices[32];
};

//-----------------------------------------------------------------------------
// Resources.
//-----------------------------------------------------------------------------
groupshared PayloadParam    s_Payload;
ConstantBuffer<SceneParam>  CbScene         : register(b0);
ConstantBuffer<MeshParam>   CbMesh          : register(b1);
ConstantBuffer<MeshletInfo> CbMeshletInfo   : register(b2);
StructuredBuffer<CullInfo>  CullInfos       : register(t0);

//-----------------------------------------------------------------------------
//      可視性をチェックします.
//-----------------------------------------------------------------------------
bool IsVisible
(
    CullInfo    cullData,
    float3      cameraPos,
    float4      planes[6],
    float4x4    world,
    float       scale
)
{
    // [-1, 1]に展開.
    float4 normalCone = UnpackSnorm4(cullData.NormalCone);

    // ワールド空間に変換.
    float3 center = mul(world, float4(cullData.BoundingSphere.xyz, 1.0f)).xyz;
    float3 axis = normalize(mul((float3x3)world, normalCone.xyz));

    // スケールを考慮した半径を求める.
    float radius = cullData.BoundingSphere.w * scale;

    // 視錐台カリング.
    if (IsCull(planes, float4(center.xyz, radius)))
    { return false; }

    // 縮退チェック.
    if (IsConeDegenerate(cullData.NormalCone))
    { return true; }

    // 視線ベクトルを求める.
    float3 viewDir = normalize(cameraPos - center);

    // 法錐カリング.
    if (IsNormalConeCull(float4(axis, normalCone.w), viewDir))
    { return false; }

    return true;
}

//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//-----------------------------------------------------------------------------
[numthreads(32, 1, 1)]
void main(uint dispatchId : SV_DispatchThreadID)
{
    bool visible = false;

    // メッシュレットカリング.
    if (dispatchId < CbMeshletInfo.MeshletCount)
    {
    #ifdef DEBUG_CULLING
        visible = IsVisible(
            CullInfos[dispatchId],
            CbScene.DebugCameraPos,
            CbScene.DebugPlanes,
            CbMesh.World,
            CbMesh.Scale);
    #else
        visible = IsVisible(
            CullInfos[dispatchId],
            CbScene.CameraPos,
            CbScene.Planes,
            CbMesh.World,
            CbMesh.Scale);
    #endif
    }

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

    uint visibleCount = WaveActiveCountBits(visible);
    DispatchMesh(visibleCount, 1, 1, s_Payload);
}

Amplificationシェーダの実装としては,まずメッシュレット数を超えないようにガードしつつ,IsVisible()メソッドにより可視判定を行います。visibleだった場合は,WavePrefixCountBits()メソッドを使って,インデックスを算出して,ペイロードにメッシュレット番号を格納します。「ペイロードってなんだよ?」って思ってググってみたら,次のような文が書かれていました。

ペイロードとは、IT用語としては、パケット通信においてパケットに含まれるヘッダやトレーラなどの付加的情報を除いた、データ本体のことである。パケットにはデータの転送先や転送経路などを制御するための情報を含むヘッダや、データの破損などを検査するトレーラなどの情報が、データそのもののほかに付加されて送られる。(weblio辞書より)

要は受け渡しデータのことですかね?当然ながらAmplificationシェーダでカリングするとデータの順番が変わってしまうので,変わったデータをメッシュシェーダに伝える必要があります。そのための受け渡し手段がペイロードという感じですかね。ちなみにペイロードの最大サイズは16Kバイトという仕様の制限があるので,注意してください。
 上記のシェーダコードで登場するWavePrefixCountBits()やWaveActibeCountBints()はシェーダモデル6.0から導入されたWave Intrinsicsという新しい組み込み関数群のうちの1つです。Wave Intrinsicsについては[Takeshige 2020]に詳しい説明が載っているので,一読しておくとよいでしょう。WavePrefixCountBits()は自身のレーン番号未満のアクティブレーンで,引数にtrueを指定した個数を返す関数です。ここでは,visibleがtrueとなった数を数えるために使用しています。WaveActiveCountBits()は引数にtrueを指定したレーンの数をすべてのアクティブレーンに返します。この関数を使ってvisibleがtrueとなった総数を求めています。あとはDispatchMesh()メソッドをシェーダから呼び出し,メッシュシェーダを起動します。1番目から3番目の引数はスレッドグループサイズです。4番目の引数はペイロードになります。IsVisible()メソッド内部で呼んでいるIsConeDegenerate()メソッドですが,これは法錐の軸ベクトルがゼロベクトルかどうかをチェックしています。この場合は,法錐カリングを正しく実行できないので,「見える」と判定してリターンします。

4 Mesh Shaderの準備

続いてメッシュシェーダです。メッシュシェーダでは,Amplificationシェーダから送られてきたペイロードを使ってメッシュレットを描画します。実装は下記のような感じです。

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include "Math.hlsli"


///////////////////////////////////////////////////////////////////////////////
// MSOutput structure
///////////////////////////////////////////////////////////////////////////////
struct MSOutput
{
    float4  Position    : SV_POSITION;
    float2  TexCoord    : TEXCOORD;
    float3  Normal      : NORMAL;
    float3  Tangent     : TANGENT;
};

///////////////////////////////////////////////////////////////////////////////
// Meshlet structure
///////////////////////////////////////////////////////////////////////////////
struct Meshlet
{
    uint VertexOffset;      // 頂点番号オフセット.
    uint VertexCount;       // 出力頂点数.
    uint PrimitiveOffset;   // プリミティブ番号オフセット.
    uint PrimitiveCount;    // 出力プリミティブ数.
};

///////////////////////////////////////////////////////////////////////////////
// MeshParam structure
///////////////////////////////////////////////////////////////////////////////
struct MeshParam
{
    float4x4 World;
    float    Scale;
    float3   Padding0;
};

///////////////////////////////////////////////////////////////////////////////
// SceneParam structure
///////////////////////////////////////////////////////////////////////////////
struct SceneParam
{
    float4x4    View;
    float4x4    Proj;
    float4      Planes[6];
    float3      CameraPos;
    float       Padding0;
    float3      DebugCameraPos;
    float       Padding1;
    float4      DebugPlanes[6];
};

///////////////////////////////////////////////////////////////////////////////
// PayloadParam structure
///////////////////////////////////////////////////////////////////////////////
struct PayloadParam
{
    uint MeshletIndices[32];
};

//-----------------------------------------------------------------------------
// Resources
//-----------------------------------------------------------------------------
StructuredBuffer<float3>    Positions       : register(t0);
StructuredBuffer<uint>      TangentSpaces   : register(t1);
StructuredBuffer<uint>      TexCoords       : register(t2);
StructuredBuffer<uint>      Indices         : register(t3);
StructuredBuffer<uint>      Primitives      : register(t4);
StructuredBuffer<Meshlet>   Meshlets        : register(t5);
ConstantBuffer<SceneParam>  CbScene         : register(b0);
ConstantBuffer<MeshParam>   CbMesh          : register(b1);

//-----------------------------------------------------------------------------
//      メインエントリーポイント.
//-----------------------------------------------------------------------------
[numthreads(128, 1, 1)]
[outputtopology("triangle")]
void main
(
    uint groupThreadId  : SV_GroupThreadID,
    uint groupId        : SV_GroupId,
    in   payload  PayloadParam payloadParam,
    out  vertices MSOutput verts[64],
    out  indices  uint3    polys[126]
)
{
    uint meshletIndex = payloadParam.MeshletIndices[groupId];
    Meshlet m = Meshlets[meshletIndex];

    SetMeshOutputCounts(m.VertexCount, m.PrimitiveCount);

    if (groupThreadId < m.PrimitiveCount)
    {
        uint packedIndex = Primitives[m.PrimitiveOffset + groupThreadId];
        polys[groupThreadId] = UnpackPrimitiveIndex(packedIndex);
    }
    
    if (groupThreadId < m.VertexCount)
    {
        uint index = Indices[m.VertexOffset + groupThreadId];
        float4 localPos = float4(Positions[index], 1.0f);
        float4 worldPos = mul(CbMesh.World, localPos);
        float4 viewPos  = mul(CbScene.View, worldPos);
        float4 projPos  = mul(CbScene.Proj, viewPos);

        float3 T, N;
        uint encodedTBN = TangentSpaces[index];
        UnpackTN(encodedTBN, T, N);

        float3 worldT = normalize(mul((float3x3)CbMesh.World, T));
        float3 worldN = normalize(mul((float3x3)CbMesh.World, N));

        MSOutput output = (MSOutput)0;
        output.Position     = projPos;
        output.TexCoord     = UnpackHalf2(TexCoords[index]);
        output.Normal       = worldN;
        output.Tangent      = worldT;

        verts[groupThreadId] = output;
    }
}

ペイロードからメッシュレット番号を引っ張り出して,その番号でメッシュレットの構造化バッファにアクセスしデータを取り出します。あとは前回やったのと同じ要領でプリミティブ格納処理や頂点加工処理を実装していけばよいです。

5 おわりに

一応簡易ですが,ちゃんとカリングできているか確認用にカメラの更新を止めるデバッグ機能も付けてみました。

通常描画
図 5: 通常描画

上記の方向から描画したものをカメラの更新を止めて,下の方から覗いてみると…

デバッグ描画
図 6: デバッグ描画

一応,それなりにちゃんと動作しているようです。
 今回はメッシュレットカリングを実装してみました。これでナウい描画方法も身につけたので,次回以降はガンガンに使うとします。

6 参考文献

サンプルコード

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