# How to Implement Meshlet Cluster Culling using meshopt_computeMeshletBounds

> Learn to implement meshlet cluster culling with meshopt_computeMeshletBounds. Optimize performance by transforming bounds and performing rejection tests.

- Repository: [Arseny Kapoulkine/meshoptimizer](https://github.com/zeux/meshoptimizer)
- Tags: how-to-guide
- Published: 2026-07-11

---

**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`](https://github.com/zeux/meshoptimizer/blob/main/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`](https://github.com/zeux/meshoptimizer/blob/main/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.

```cpp
// 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.

```cpp
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`](https://github.com/zeux/meshoptimizer/blob/main/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`](https://github.com/zeux/meshoptimizer/blob/main/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:

```cpp
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`](https://github.com/zeux/meshoptimizer/blob/main/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`](https://github.com/zeux/meshoptimizer/blob/main/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.