アヒルのある日

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

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

こんにちは、みにくい社長です。
今回も引き続き、メッシュシェーダーの実装を解説します。

③ASでメッシュレットをカリングする

AmplificationShaderを使ってメッシュレットをカリングすることで、従来の描画フローではできなかった細かい単位でのカリングが可能になります。
視錘台カリングや背面カリングだけでも効果はありますが、HZB(階層Zバッファ)を使用して遮蔽カリングを行えば、シーンによっては大きな効果が得られます。
AmplificationShaderは下記のようになります。

float4 gMeshParameter;   // x: meshletのオフセット, y:HZBのミップ数
float4 gFrustumPlanes[6]; // 視錐台平面
Texture2D gDepthStencilMap;
struct Payload
{
    uint3   _groupID;
};
groupshared Payload gPayload;

// 可視チェック(視錘台、背面)
bool isVisible(Meshlet meshlet)
{
    float3 centerView = mul(gWorldViewMatrix[0], meshlet._boundingSphereCenter);
    // 視錘台カリング
    float3 center = mul(gInversViewMatrix, centerView);
    [unroll]
    for (int i = 0; i < 6; ++i)
    {
        float4 plane = gFrustumPlanes[i];
        float dist = dot(plane.xyz, center) + plane.w;

        if (dist < -meshlet._boundingSphereRadius)
        {
            return false;
        }
    }
    // 背面カリング
    float3 coneAxis = float3(
            (int)((meshlet._coneAxisCutoff << 24) >> 24),
            (int)((meshlet._coneAxisCutoff << 16) >> 24),
            (int)((meshlet._coneAxisCutoff << 8) >> 24)
        ) / 127.0f;
    float coneCutoff = (int)(meshlet._coneAxisCutoff >> 24) / 127.0f;
    if(!all(coneAxis == 0.0f) && coneCutoff != 0.0f)
    {
        float3 coneAxisView = mul((float3x3)gWorldViewMatrix[0], coneAxis);
        float3 coneAxisWorld = mul((float3x3)gInversViewMatrix, coneAxisView);
        float3 viewDir = -float3(gInversViewMatrix[0][2], gInversViewMatrix[1][2], gInversViewMatrix[2][2]);
        if (dot(coneAxisWorld, -viewDir) < -coneCutoff)
        {
            return false;
        }
    }
    return true;
}

// 可視チェック(遮蔽)
bool isVisibleOcclusion(Meshlet meshlet)
{
    // 球より半径分だけ手前の座標を基準にする
    float3 centerView = mul(gWorldViewMatrix[0], meshlet._boundingSphereCenter);
    centerView = normalize(centerView) * (length(centerView) - meshlet._boundingSphereRadius);
    // 遮蔽カリング
    float4 centerProjection = mul(gProjectionMatrix, float4(centerView, 1.0f));
    float depth = centerProjection.z / centerProjection.w * 0.5f;
    float screenRadius = meshlet._boundingSphereRadius / centerProjection.w * 0.5f * gScreenParameter.x;
    if(depth >= 0 && screenRadius >= 0) // note: 外れ値は対象外
    {
        float2 screenPosition = centerProjection.xy + 0.5 / gScreenParameter.xy;
        uint level = clamp((uint)log2(screenRadius * 2), 0, gMeshParameter.y - 1);
        float hzbDepth = gDepthStencilMap.Load(int3(screenPosition / (1 << level), level));
        if(depth > hzbDepth + 0.001f)
        {
            return false;
        }
    }
    return true;
}
//---------------------------------------------------------------------------
// 増幅シェーダー
//---------------------------------------------------------------------------
[numthreads(MESH_SHADER_THREAD_MAX, MESH_SHADER_INSTANCE_MAX, 1)]
void AS_MAIN(uint dispatchId : SV_DispatchThreadID, uint3 groupID : SV_GroupID)
{
    gPayload._groupID = groupID;
    bool visible = isVisible(gMeshletBuffer[gMeshParameter.x + groupID.x]) && isVisibleOcclusion(gMeshletBuffer[gMeshParameter.x + groupID.x]);
    DispatchMesh(visible ? 1 : 0, 1, 1, gPayload);
}
//---------------------------------------------------------------------------
//  メッシュシェーダー
//---------------------------------------------------------------------------
[numthreads(MESH_SHADER_THREAD_MAX, MESH_SHADER_INSTANCE_MAX, 1)]
[outputtopology("triangle")]
void MS_MAIN(uint threadID : SV_GroupIndex, in payload Payload payload, out vertices Interpolator outVertices[MESH_SHADER_VERTEX_MAX], out indices uint3 outIndices[MESH_SHADER_INDEX_MAX])
{
    Meshlet meshlet = gMeshletBuffer[gMeshParameter.x + payload._groupID.x];
    :
}

増幅シェーダーはメッシュシェーダーの前に実行され、C++で呼び出していたDispatchMeshを増幅シェーダーから呼び出すかどうかで、メッシュシェーダーの実行可否が変わります。つまり、カリングされた場合はメッシュシェーダーが呼び出されない為、その分の処理負荷が下がります。 増幅シェーダーからメッシュシェーダーを呼び出す場合は、Payloadを使用して情報を渡す必要があるので注意して下さい。
後はisVisible関数でカリング判定を行うだけです。
視錘台カリングは、視錐台の6面の情報をc++から渡して判定します。ビュー射影行列からこのように計算します。

pFrustumPlanes[0] = Vector4(viewProjMatrix._m[0][3] + viewProjMatrix._m[0][0], viewProjMatrix._m[1][3] + viewProjMatrix._m[1][0], viewProjMatrix._m[2][3] + viewProjMatrix._m[2][0], viewProjMatrix._m[3][3] + viewProjMatrix._m[3][0]); //left
pFrustumPlanes[1] = Vector4(viewProjMatrix._m[0][3] - viewProjMatrix._m[0][0], viewProjMatrix._m[1][3] - viewProjMatrix._m[1][0], viewProjMatrix._m[2][3] - viewProjMatrix._m[2][0], viewProjMatrix._m[3][3] - viewProjMatrix._m[3][0]); //right
pFrustumPlanes[2] = Vector4(viewProjMatrix._m[0][3] + viewProjMatrix._m[0][1], viewProjMatrix._m[1][3] + viewProjMatrix._m[1][1], viewProjMatrix._m[2][3] + viewProjMatrix._m[2][1], viewProjMatrix._m[3][3] + viewProjMatrix._m[3][1]); //bottom
pFrustumPlanes[3] = Vector4(viewProjMatrix._m[0][3] - viewProjMatrix._m[0][1], viewProjMatrix._m[1][3] - viewProjMatrix._m[1][1], viewProjMatrix._m[2][3] - viewProjMatrix._m[2][1], viewProjMatrix._m[3][3] - viewProjMatrix._m[3][1]); //top
pFrustumPlanes[4] = Vector4(viewProjMatrix._m[0][3] + viewProjMatrix._m[0][2], viewProjMatrix._m[1][3] + viewProjMatrix._m[1][2], viewProjMatrix._m[2][3] + viewProjMatrix._m[2][2], viewProjMatrix._m[3][3] + viewProjMatrix._m[3][2]); //near
pFrustumPlanes[5] = Vector4(viewProjMatrix._m[0][3] - viewProjMatrix._m[0][2], viewProjMatrix._m[1][3] - viewProjMatrix._m[1][2], viewProjMatrix._m[2][3] - viewProjMatrix._m[2][2], viewProjMatrix._m[3][3] - viewProjMatrix._m[3][2]); //far
for (uint i = 0; i < static_cast<uint>(RENDER_CUBEMAP_FACE::MAX); i++)
{
    const float length = sqrt(pFrustumPlanes[i]._x * pFrustumPlanes[i]._x + pFrustumPlanes[i]._y * pFrustumPlanes[i]._y + pFrustumPlanes[i]._z * pFrustumPlanes[i]._z);
    pFrustumPlanes[i] /= length;
}

背面カリングは、メッシュレット分割時に保存した法線をまとめた円錐の情報から判定できます。
遮蔽カリングではHZBを使用し、バウンディングスフィアがピクセルに収まるミップレベルでZ判定を行います。 HZBは通常のZバッファからミップマップを生成しますが、下記のようなコンピュートシェーダーでミップレベル数分だけDispatch呼び出しをします。

float4 gHZBResolution;   // xy: 読み込む解像度, zw: 書き込む解像度
float4 gHZBParameter;   // x: 読み込むレベル
Texture2D<float> gDepthStencilMap;
RWTexture2D<float> gHZBMap;

[numthreads(8, 8, 1)]
void shaderMain(uint3 dispatchThreadID : SV_DispatchThreadID)
{
    if (dispatchThreadID.x < gHZBResolution.z && dispatchThreadID.y < gHZBResolution.w)
    {
        uint2 srcBase = dispatchThreadID.xy * 2;
        uint2 srcMax = gHZBResolution.xy - 1;

        float d00 = gDepthStencilMap.Load(int3(min(srcBase, srcMax), gHZBParameter.x));
        float d10 = gDepthStencilMap.Load(int3(min(srcBase + uint2(1, 0), srcMax), gHZBParameter.x));
        float d01 = gDepthStencilMap.Load(int3(min(srcBase + uint2(0, 1), srcMax), gHZBParameter.x));
        float d11 = gDepthStencilMap.Load(int3(min(srcBase + uint2(1, 1), srcMax), gHZBParameter.x));

        gHZBMap[dispatchThreadID.xy] = max(max(d00, d10), max(d01, d11));
    }
}

これでメッシュシェーダーへの切り替えは一通り完了です。
速度メリットを得るにはもう少し改善が必要かもしれませんが、ここまでの実装でとりあえず使えるものにはなると思います。 GPU駆動レンダリングはこれからどんどん発展していきますので、既存ワークフローを崩さずに切り替えて行くことをお勧めします。
では、またねー。