こんにちは、みにくい社長です。
前回の続きでメッシュシェーダーのハイブリッド実装を解説します。
②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);
これでメッシュシェーダー対応は完了です。
メッシュレットの分割やシェーダーパラメータの設定の所は少し手間がかかりますが、
今までのワークフローを崩さずに対応できるように配慮されているのがわかるかと思います。
但し、ここまでの対応だけでは、メッシュシェーダーのメリットは特にありません。
恐らく、処理負荷は従来よりも上がってしまうと思います。
メッシュシェーダーのメリットを得るには、メッシュレット単位のカリングが必要です。
続きは次回ということで、またねー。