アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

メッシュシェーダーのハイブリッド実装②

こんにちは、みにくい社長です。
前回の続きでメッシュシェーダーのハイブリッド実装を解説します。

②MSでメッシュレットを描画する

前回はメッシュレットの描画の準備の為にメッシュレット分割をする方法を紹介しました。今回はメッシュレットの描画をやります。
メッシュレットを描画するだけであれば、AmplificationShaderは不要なのでMeshShaderだけを用意します。

struct Meshlet
{
    uint    _vertexOffset;
    uint    _triangleOffset;
    uint    _vertexCount;
    uint    _triangleCount;
    float3  _boundingSphereCenter;
    float   _boundingSphereRadius;
    int     _coneAxisCutoff;
};

struct VertexInput
{
    float3  _position;
    uint    _color;
    float3  _normal;
    float3  _tangent;
    float3  _binormal;
    float2  _uv;
    float2  _uv2;
    uint    _blendWeight;
    uint    _jointIndices;
};

StructuredBuffer<VertexInput> gVertexBuffer;
StructuredBuffer<Meshlet> gMeshletBuffer;
StructuredBuffer<uint16_t> gIndexBuffer;
StructuredBuffer<uint> gTriangleBuffer;
float4 gMeshParameter;  // x: meshletのオフセット

uint getTriangleIndex(uint index)
{
    return (gTriangleBuffer[index / 4] >> (8 * (index % 4))) & 0xFF;
}

#define MESH_SHADER_THREAD_MAX 16
#define MESH_SHADER_INSTANCE_MAX 8
#define MESH_SHADER_VERTEX_MAX 128
#define MESH_SHADER_INDEX_MAX 128
//---------------------------------------------------------------------------
//  メッシュシェーダー
//---------------------------------------------------------------------------
[numthreads(MESH_SHADER_THREAD_MAX, MESH_SHADER_INSTANCE_MAX, 1)]
[outputtopology("triangle")]
void MS_MAIN(uint threadID : SV_GroupIndex, out vertices Interpolator outVertices[MESH_SHADER_VERTEX_MAX], out indices uint3 outIndices[MESH_SHADER_INDEX_MAX])
{
    Meshlet meshlet = gMeshletBuffer[gMeshParameter.x + groupID.x];
    SetMeshOutputCounts(meshlet._vertexCount, meshlet._triangleCount);
    if(threadID < meshlet._vertexCount)
    {
        uint vertexId = meshlet._vertexOffset + threadID;
        Input input = gVertexBuffer[gIndexBuffer[vertexId]];
        outVertices[threadID] = VS_MAIN(input);
    }
    if(threadID < meshlet._triangleCount)
    {
        uint triangleId = meshlet._triangleOffset + threadID * 3;
        outIndices[threadID] = uint3(getTriangleIndex(triangleId), getTriangleIndex(triangleId + 1), getTriangleIndex(triangleId + 2));
    }
}

StructuredBufferは任意の構造体配列をシェーダーに渡す時に使います。 今回使用している4つのStructuredBufferは、前回のメッシュレット分割で作成したデータです。
gMeshParameterはxにメッシュレット毎のオフセット値を格納しています。
メッシュシェーダーのスレッドの使い方はいくつか方法がありますが、今回はわかりやすさ優先で

[numthreads(MESH_SHADER_THREAD_MAX, MESH_SHADER_INSTANCE_MAX, 1)]

としています。
シェーダーの中身は、SetMeshOutputCountsでメッシュレットの出力頂点数、トライアングル数を設定し、outVerticesとoutIndicesに頂点単位で座標とインデックスを出力します。頂点座標は従来の頂点シェーダーをそのまま呼び出せば良いので簡単ですが、頂点フォーマットがモデルによって異なる場合はフォーマットのズレをどうにかして吸収する必要があります。
注意点として、SetMeshOutputCountsは必須の命令で、分岐の中に記述してはいけません。
また、これはメッシュレット分割時の注意点になるのですが、メッシュレットを跨いでインデックス参照を行うことはできません。分割時は128頂点の中で完結するように分割しましょう。(クラッシュはしないので、原因が特定に時間がかかりました)
cpp側の呼び出しは下記になります。 インスタンシングを行わない場合はinstanceCountは1にしています。

_pD3D12GraphicsCommandList->DispatchMesh(meshletCount, instanceCount, 1);

これでメッシュシェーダー対応は完了です。
メッシュレットの分割やシェーダーパラメータの設定の所は少し手間がかかりますが、 今までのワークフローを崩さずに対応できるように配慮されているのがわかるかと思います。
但し、ここまでの対応だけでは、メッシュシェーダーのメリットは特にありません。 恐らく、処理負荷は従来よりも上がってしまうと思います。 メッシュシェーダーのメリットを得るには、メッシュレット単位のカリングが必要です。

続きは次回ということで、またねー。