How to Implement Meshlet Cluster Culling using meshopt_computeMeshletBounds

Call meshopt_computeMeshletBounds once per meshlet during preprocessing to generate a meshopt_Bounds structure containing sphere, cone, and AABB data; transform these bounds to camera space at runtime to reject clusters with frustum, distance, or back-face cone tests.

The zeux/meshoptimizer library provides the building blocks for GPU-driven mesh shading pipelines. The meshopt_computeMeshletBounds function, declared in src/meshoptimizer.h at line 818, computes tight bounding volumes for individual meshlet clusters. These bounds enable renderers to cull groups of triangles before expensive vertex shading, which is essential for modern cluster-based LOD systems.

Understanding the meshopt_Bounds Structure

The function returns a meshopt_Bounds structure defined at line 777 of src/meshoptimizer.h. This structure provides three separate bounding representations that cover different culling strategies:

  • Sphere culling: center (float3) and radius (float) define a bounding sphere for distance-based rejection.
  • Cone culling: cone_axis, cone_apex, and cone_cutoff define a tight view cone that encapsulates all triangle normals, enabling back-face and view-frustum culling.
  • AABB culling: box_min and box_max provide an axis-aligned bounding box for conservative frustum tests.

Because the bounds are computed in object space, you must transform them by the instance’s world matrix before performing camera-space culling.

Building Meshlets with meshopt_buildMeshlets

Before computing bounds, you must partition the mesh into small clusters using one of the meshlet builders. The meshopt_buildMeshletsSpatial function creates spatially coherent meshlets that are ideal for cluster culling.

// Calculate maximum possible meshlets
size_t max_meshlets = meshopt_buildMeshletsBound(
    indexCount, 
    config.max_vertices, 
    config.max_triangles);

std::vector<meshopt_Meshlet> meshlets(max_meshlets);
std::vector<unsigned int> meshlet_vertices(max_meshlets * config.max_vertices);
std::vector<unsigned char> meshlet_triangles(max_meshlets * config.max_triangles * 3);

// Build the meshlets
size_t actual = meshopt_buildMeshletsSpatial(
    meshlets.data(),
    meshlet_vertices.data(),
    meshlet_triangles.data(),
    indices,
    indexCount,
    vertex_positions,
    vertexCount,
    sizeof(float) * 3,  // stride
    config.max_vertices,
    config.min_triangles,
    config.max_triangles,
    config.fill_weight);

meshlets.resize(actual);

This produces an array of meshopt_Meshlet descriptors and dense vertex/triangle buffers that reference the original vertex positions.

Computing Per-Meshlet Bounds

Iterate through the generated meshlets and call meshopt_computeMeshletBounds for each cluster. This function accepts the meshlet’s local vertex and triangle indices along with the global vertex position buffer.

std::vector<meshopt_Bounds> meshletBounds(actual);

for (size_t i = 0; i < actual; ++i) {
    const meshopt_Meshlet& m = meshlets[i];
    
    // Pointers to this meshlet's data within the global buffers
    const unsigned int* verts = &meshlet_vertices[m.vertex_offset];
    const unsigned char* tris = &meshlet_triangles[m.triangle_offset];

    meshletBounds[i] = meshopt_computeMeshletBounds(
        verts,
        tris,
        m.triangle_count,
        vertex_positions,
        vertexCount,
        sizeof(float) * 3);
}

Key parameters:

  • meshlet_vertices: Pointer to the unsigned int array of vertex indices used by this specific meshlet.
  • meshlet_triangles: Packed unsigned char array containing 3 indices per triangle.
  • triangle_count: Number of triangles in this meshlet.
  • vertex_positions: The original float3 position array for the entire mesh.
  • vertex_positions_stride: Byte stride between consecutive positions (typically 12 for packed float3).

The implementation in src/meshoptimizer.cpp computes the optimal bounding sphere, normal cone, and AABB by analyzing the actual geometry referenced by the meshlet indices.

Implementing Cluster Culling in Your Renderer

Store the resulting meshopt_Bounds array alongside your meshlet descriptors. At render time, transform the bounds to view space and perform hierarchical culling tests. The clusterlod demo in demo/clusterlod.h demonstrates this pattern for LOD selection.

Here is a complete view-cone culling example that uses both the sphere and cone components:

bool isMeshletVisible(const meshopt_Bounds& b, 
                      const float3& camPos,
                      const float3& camDir,
                      float fovCos) {
    // Transform center to view space (world matrix pre-applied)
    float3 toCenter = b.center - camPos;
    float dist = dot(toCenter, camDir);
    
    // Distance/sphere culling
    if (dist < -b.radius) return false;
    
    // Cone culling: reject if meshlet faces away from camera
    float3 axis = normalize(b.cone_axis);
    float cosAngle = dot(axis, camDir);
    
    // cone_cutoff is the cosine of the half-angle of the normal cone
    return cosAngle > b.cone_cutoff;
}

// Render loop
for (size_t i = 0; i < actual; ++i) {
    if (!isMeshletVisible(meshletBounds[i], camera.pos, camera.dir, fovCos))
        continue;
        
    // Issue indirect draw or mesh shader dispatch for this meshlet
    drawMeshlet(meshlets[i]);
}

For frustum culling, test the box_min and box_max against the six clip planes after transforming the AABB to world space.

Summary

  • meshopt_computeMeshletBounds in src/meshoptimizer.h (line 818) computes object-space bounding volumes for meshlet clusters.
  • The returned meshopt_Bounds structure provides sphere, cone, and AABB data for flexible culling strategies.
  • Call this function once per meshlet during asset preprocessing, not per frame, to minimize overhead.
  • The clusterlod demo provides a reference implementation of hierarchical cluster culling using these bounds.
  • Transform bounds to camera space before testing to support instanced rendering and moving objects.

Frequently Asked Questions

What culling methods does meshopt_Bounds support?

The structure supports sphere culling for distance checks, normal cone culling for back-face rejection, and AABB frustum culling for view-frustum clipping. The cone test is particularly effective for mesh shading pipelines because it can eliminate entire clusters of back-facing triangles with a single dot product.

How expensive is it to call meshopt_computeMeshletBounds?

The function is designed to be called once during build time, not per frame. It performs a linear scan of the meshlet’s triangles to compute optimal bounds, which is inexpensive relative to the cost of building the meshlets themselves. According to the implementation in src/meshoptimizer.cpp, the cost scales linearly with the number of triangles in the meshlet, making it suitable for preprocessing pipelines.

Can I use meshopt_computeMeshletBounds with custom vertex formats?

Yes, provided your vertex positions are accessible as a contiguous array of floats. Use the vertex_positions_stride parameter to specify the byte offset between consecutive positions. For example, if your vertex structure is { float3 pos; float2 uv; float3 normal; }, pass sizeof(Vertex) as the stride and ensure vertex_positions points to the first position component.

How does the cone culling in meshopt_Bounds work?

The cone_axis and cone_cutoff define a cone that tightly bounds all triangle normals in the meshlet. If the view direction dot cone_axis is greater than cone_cutoff, the camera is looking at the front-facing side of the cluster. This test, demonstrated in the clusterlod demo, allows you to skip meshlets that face away from the camera without testing individual triangles.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →