Skip to the content.
Posts

First published: 2024-02-22
Last updated: 2024-02-22

About the Mesh Shading Series

This post is part 6 of a series about mesh shading. My intent in this series is to introduce the various parts of mesh shading in an easy to understand fashion. Well, as easy as I can make it. My objective isn’t to convince you to use mesh shading. I assume you’re reading this post because you’re already interested in mesh shading. Instead, my objective is to explain the mechanics of how to do mesh shading in Direct3D 12, Metal, and Vulkan as best I can. My hope is that you’re able to use this information in your own graphics projects and experiments.

Sample Projects for This Post

116_mesh_shader_calc_lod - Demonstrates how to calculate LOD using distance to camera during amplification.

The D3D12 version of the above samples displays pipeline statistics. The Metal and Vulkan versions do not display pipeline statistics for different reasons. Metal doesn’t have pipeline statistics. Turning on pipeline statistics on the Vulkan version tanks the performance. I haven’t had a chance to investigate why this is and how it affects the various GPUs.

Introduction

This post is an immediate follow up to Mesh Shading Part 5: LOD Selection and discusses how to automatically select an LOD using distance to camera.

There’s obviously other ways to determine which LOD to use for drawing, such as screen pixel coverage. However, in the interest of keeping things simple, we’ll use distance to camera. It’s straightforward and easy to understand…and also is a very small amount of code.

Let’s jump in!

Camera Position and Meshlet Bounds

We need to add a few fields to the SceneProperties struct to do the LOD calculation:

Here’s what SceneProperties looks like with the new fields:

// -----------------------------------------------------------------------------
// D3D12 and Vulkan
// -----------------------------------------------------------------------------
struct SceneProperties
{
    float3   EyePosition;
    uint     __pad0;
    float4x4 CameraVP;
    uint     InstanceCount;
    uint     MeshletCount;
    uint     __pad1;
    float    MaxLODDistance;         // Use least detail level at or beyond this distance
    uint4    Meshlet_LOD_Offsets[5]; // Align array element to 16 bytes
    uint4    Meshlet_LOD_Counts[5];  // Align array element to 16 bytes
    float3   MeshBoundsMin;
    uint     __pad2;
    float3   MeshBoundsMax;
};

// -----------------------------------------------------------------------------
// Metal
// -----------------------------------------------------------------------------
//
// NOTE: Unlike D3D12 and Vulkan, it looks like Metal arrays are tightly
//       packed for 32-bit scalar types. This means that Meshlet_LOD_Offsets
//       and Meshlet_LOD_Counts are uint here instead of uint4/uvec4.
//
struct SceneProperties
{
    float3      EyePosition;
    uint        __pad0;
    float4x4    CameraVP;
    uint        InstanceCount;
    uint        MeshletCount;
    uint        __pad1;
    float       MaxLODDistance;         // Use least detail level at or beyond this distance
    uint        Meshlet_LOD_Offsets[5]; // Align array element to 16 bytes
    uint        Meshlet_LOD_Counts[5];  // Align array element to 16 bytes
    float3      MeshBoundsMin;
    float3      MeshBoundsMax;
};

The LOD calculation starts by transforming MeshBoundsMin and MeshBoundsMax from object space to world space. Then the distance between EyePosition and the center of the world space mesh bounds is used to determine which LOD is used. If the distance is equal to or greater than MaxLODDistance, the LOD with the least detail is used. In our case this is LOD 4. We’ll see how this works in the amplification shader in just a bit.

Updating Scene Constant Data

We add a few lines of code here to set EyePosition, MaxLODDistance, MeshBoundsMin, and MeshBoundsMax.

// -----------------------------------------------------------------------------
// D3D12 and Vulkan
// -----------------------------------------------------------------------------
scene.EyePosition              = camera.GetEyePosition(); // ** NEW **
scene.CameraVP                 = camera.GetViewProjectionMatrix();
scene.InstanceCount            = static_cast<uint32_t>(instances.size());
scene.MeshletCount             = meshlet_LOD_Counts[0];
scene.MaxLODDistance           = gMaxLODDistance;
scene.Meshlet_LOD_Offsets[0].x = meshlet_LOD_Offsets[0];
scene.Meshlet_LOD_Offsets[1].x = meshlet_LOD_Offsets[1];
scene.Meshlet_LOD_Offsets[2].x = meshlet_LOD_Offsets[2];
scene.Meshlet_LOD_Offsets[3].x = meshlet_LOD_Offsets[3];
scene.Meshlet_LOD_Offsets[4].x = meshlet_LOD_Offsets[4];
scene.Meshlet_LOD_Counts[0].x  = meshlet_LOD_Counts[0];
scene.Meshlet_LOD_Counts[1].x  = meshlet_LOD_Counts[1];
scene.Meshlet_LOD_Counts[2].x  = meshlet_LOD_Counts[2];
scene.Meshlet_LOD_Counts[3].x  = meshlet_LOD_Counts[3];
scene.Meshlet_LOD_Counts[4].x  = meshlet_LOD_Counts[4];
scene.MeshBoundsMin            = float3(meshBounds.min); // ** NEW **
scene.MeshBoundsMax            = float3(meshBounds.max); // ** NEW **

// -----------------------------------------------------------------------------
// Metal
// -----------------------------------------------------------------------------
scene.EyePosition             = camera.GetEyePosition(); // ** NEW **
scene.CameraVP                = camera.GetViewProjectionMatrix();
scene.InstanceCount           = static_cast<uint32_t>(instances.size());
scene.MeshletCount            = meshlet_LOD_Counts[0];
scene.MaxLODDistance          = gMaxLODDistance;
scene.Meshlet_LOD_Offsets[0]  = meshlet_LOD_Offsets[0];
scene.Meshlet_LOD_Offsets[1]  = meshlet_LOD_Offsets[1];
scene.Meshlet_LOD_Offsets[2]  = meshlet_LOD_Offsets[2];
scene.Meshlet_LOD_Offsets[3]  = meshlet_LOD_Offsets[3];
scene.Meshlet_LOD_Offsets[4]  = meshlet_LOD_Offsets[4];
scene.Meshlet_LOD_Counts[0]   = meshlet_LOD_Counts[0];
scene.Meshlet_LOD_Counts[1]   = meshlet_LOD_Counts[1];
scene.Meshlet_LOD_Counts[2]   = meshlet_LOD_Counts[2];
scene.Meshlet_LOD_Counts[3]   = meshlet_LOD_Counts[3];
scene.Meshlet_LOD_Counts[4]   = meshlet_LOD_Counts[4];
scene.MeshBoundsMin           = float3(meshBounds.min); // ** NEW **
scene.MeshBoundsMax           = float3(meshBounds.max); // ** NEW **

Instance Positions

We’re still using the 5 instances to show the different LODS. However, the instances are purposely placed so they fall into one of the LODs.

// Update instance transforms
{
    float maxSpan       = std::max<float>(meshBounds.Width(), meshBounds.Depth());
    float instanceSpanX = 4.0f * maxSpan;
    float instanceSpanZ = 4.5f * maxSpan;
    float totalSpanX    = kNumInstanceCols * instanceSpanX;
    float totalSpanZ    = kNumInstanceRows * instanceSpanZ;

    float t = static_cast<float>(glfwGetTime());

    // 0
    {
        float3 P     = float3(0, 0, 0);
        instances[0] = glm::translate(P) * glm::rotate(t, float3(0, 1, 0));
    }

    // 1
    {
        float3 P     = float3(0, 0, -0.75f);
        instances[1] = glm::translate(P) * glm::rotate(t, float3(0, 1, 0));
    }

    // 2
    {
        float3 P     = float3(0, 0, -3.0f);
        instances[2] = glm::translate(P) * glm::rotate(t, float3(0, 1, 0));
    }

    // 3
    {
        float3 P     = float3(0, 0, -6);
        instances[3] = glm::translate(P) * glm::rotate(t, float3(0, 1, 0));
    }

    // 4
    {
        float3 P     = float3(0, 0, -16);
        instances[4] = glm::translate(P) * glm::rotate(t, float3(0, 1, 0));
    }
}

Amplification Shader

We only need to make some changes to the amplification shader to support LODs:

  1. Add #define MAX_LOD_COUNT 5 to the top of the shader.
  2. Add the EyePosition, MaxLODDistance, MeshBoundsMin, and MeshBoundsMax to the SceneProperties struct.
  3. Update amplification shader body to calculate LOD.

Define MAX_LOD_COUNT

MAX_LOD_COUNT is a constant that specifies how many LODs we have and is used during the LOD calculation.

// Object function group size
#define AS_GROUP_SIZE 32
#define MAX_LOD_COUNT 5

Camera Position and Meshlet Bounds

Add the EyePosition, MaxLODDistance, MeshBoundsMin, and MeshBoundsMax to the shader’s SceneProperties. Note that for the Metal version we once again have to use packed_float3 so that metal doesn’t pad the 3-component vector to be 16-bytes wide.

// -----------------------------------------------------------------------------
// HLSL
// -----------------------------------------------------------------------------
struct SceneProperties {
    float3      EyePosition;
    float4x4    CameraVP;
    uint        InstanceCount;
    uint        MeshletCount;
    uint        __pad0;
    float       MaxLODDistance;
    uint        Meshlet_LOD_Offsets[5];
    uint        Meshlet_LOD_Counts[5];
    float3      MeshBoundsMin;
    float3      MeshBoundsMax;
};

// -----------------------------------------------------------------------------
// MSL
// -----------------------------------------------------------------------------
struct SceneProperties {
    packed_float3 EyePosition;
    float4x4      CameraVP;
    uint          InstanceCount;
    uint          MeshletCount;
    uint          __pad0;
    float         MaxLODDistance;
    uint          Meshlet_LOD_Offsets[5];
    uint          Meshlet_LOD_Counts[5];
    packed_float3 MeshBoundsMin;
    packed_float3 MeshBoundsMax;
};

Calculate LOD Using Distance To Camera

First thing we need to calculate the LOD is the instance’s model transform matrix.

// Instance's model transform matrix
float4x4 M  = Instances[instanceIndex].M;

Next we calculate the instance center in world space.

// Get center of transformed bounding box to use in LOD distance calculation
float4 instanceBoundsMinWS = mul(M, float4(Scene.MeshBoundsMin, 1.0));
float4 instanceBoundsMaxWS = mul(M, float4(Scene.MeshBoundsMax, 1.0));
float4 instanceCenter = (instanceBoundsMinWS + instanceBoundsMaxWS) / 2.0;

Calculate the distance between the instance center and the camera’s eye position.

// Distance between transformed bounding box and camera eye position
float dist = distance(instanceCenter.xyz, Scene.EyePosition);

Normalize the distance so it’s between 0 and 1 for the LOD calculation.

// Normalize distance using MaxLODDistance
float ndist = clamp(dist / Scene.MaxLODDistance, 0.0, 1.0);

We apply an exponential curve to fit normalize distance to the instance positions. This guarantees that each of the instances falls into a particular LOD. And that’s pretty much it, we now have the LOD we’re going to use to draw.

// Calculate LOD using normalized distance
uint lod = (uint)(pow(ndist, 0.65) * (MAX_LOD_COUNT - 1));

Full Shader Body

HLSL for D3D12 and Vulkan

[numthreads(AS_GROUP_SIZE, 1, 1)]
void asmain(
    uint gtid : SV_GroupThreadID,
    uint dtid : SV_DispatchThreadID,
    uint gid  : SV_GroupID
)
{
    bool visible = false;

    uint instanceIndex = dtid / Scene.MeshletCount;
    uint meshletIndex  = dtid % Scene.MeshletCount;

    // Make sure instance index is within bounds
    if (instanceIndex < Scene.InstanceCount) {
        // Instance's model transform matrix
        float4x4 M  = Instances[instanceIndex].M;

        // Get center of transformed bounding box to use in LOD distance calculation
        float4 instanceBoundsMinWS = mul(M, float4(Scene.MeshBoundsMin, 1.0));
        float4 instanceBoundsMaxWS = mul(M, float4(Scene.MeshBoundsMax, 1.0));
        float4 instanceCenter = (instanceBoundsMinWS + instanceBoundsMaxWS) / 2.0;

        // Distance between transformed bounding box and camera eye position
        float dist = distance(instanceCenter.xyz, Scene.EyePosition);

        // Normalize distance using MaxLODDistance
        float ndist = clamp(dist / Scene.MaxLODDistance, 0.0, 1.0);

        // Calculate LOD using normalized distance
        uint lod = (uint)(pow(ndist, 0.65) * (MAX_LOD_COUNT - 1));

        // Get meshlet count for the LOD
        uint lodMeshletCount = Scene.Meshlet_LOD_Counts[lod];

        // Make sure meshlet index is within bounds of current LOD's meshlet count
        if (meshletIndex < lodMeshletCount) {
            meshletIndex += Scene.Meshlet_LOD_Offsets[lod];
            
            // Assuming visibile, no culling here
            visible = 1;
        }
    }

    if (visible) {
        uint index = WavePrefixCountBits(visible);
        sPayload.InstanceIndices[index] = instanceIndex;
        sPayload.MeshletIndices[index]  = meshletIndex;
    }
    
    uint visibleCount = WaveActiveCountBits(visible);    
    DispatchMesh(visibleCount, 1, 1, sPayload); 
}

MSL for Metal

[[object]]
void objectMain(
    constant SceneProperties&  Scene         [[buffer(0)]],
    device const float4*       MeshletBounds [[buffer(1)]],    
    device const Instance*     Instances     [[buffer(2)]],
    uint                       gtid          [[thread_position_in_threadgroup]],
    uint                       dtid          [[thread_position_in_grid]],
    object_data Payload&       outPayload    [[payload]],
    mesh_grid_properties       outGrid)
{
    uint visible = 0;

    uint instanceIndex = dtid / Scene.MeshletCount;
    uint meshletIndex  = dtid % Scene.MeshletCount;
   
    if (instanceIndex < Scene.InstanceCount) {
        // Instance's model transform matrix
        float4x4 M  = Instances[instanceIndex].M;

        // Get center of transformed bounding box to use in LOD distance calculation
        float4 instanceBoundsMinWS = M * float4(Scene.MeshBoundsMin, 1.0);
        float4 instanceBoundsMaxWS = M * float4(Scene.MeshBoundsMax, 1.0);
        float4 instanceCenter = (instanceBoundsMinWS + instanceBoundsMaxWS) / 2.0;

        // Distance between transformed bounding box and camera eye position
        float dist = distance(instanceCenter.xyz, Scene.EyePosition);

        // Normalize distance using MaxLODDistance
        float ndist = clamp(dist / Scene.MaxLODDistance, 0.0, 1.0);

        // Calculate LOD using normalized distance
        uint lod = (uint)(pow(ndist, 0.65) * (MAX_LOD_COUNT - 1));

        // Get meshlet count for the LOD
        uint lodMeshletCount = Scene.Meshlet_LOD_Counts[lod];

        if (meshletIndex < lodMeshletCount) {
            meshletIndex += Scene.Meshlet_LOD_Offsets[lod];
            
            // Assuming visibile, no culling here
            visible = 1;
        }
    }

    if (visible) {
        uint index = simd_prefix_exclusive_sum(visible);
        outPayload.InstanceIndices[index] = instanceIndex;
        outPayload.MeshletIndices[index]  = meshletIndex;
    }

    // Assumes all meshlets are visible
    uint visibleCount = simd_sum(visible);
    outGrid.set_threadgroups_per_grid(uint3(visibleCount, 1, 1));
}

Mesh Shader Changes

There aren’t any mesh shader changes for this post. Hope it’s not too disappointing. We’ll make up for it soon.

Rendered Image

The 116_mesh_shader_calc_lod sample renders 5 instances of the horse statue at 5 different LODs using distance to camera calculation.