# How to Implement Cluster Cone Culling with meshopt_computeMeshletBounds in meshoptimizer

> Learn to implement cluster cone culling using meshopt_computeMeshletBounds in meshoptimizer. Optimize your rendering by culling back-facing clusters efficiently.

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

---

**To implement cluster cone culling with `meshopt_computeMeshletBounds`, generate meshlets using `meshopt_buildMeshlets` with a non-zero `cone_weight`, compute bounds for each meshlet using `meshopt_computeMeshletBounds`, then test if the dot product between the normalized view vector and cone axis is less than the `cone_cutoff` to reject back-facing clusters.**

The meshoptimizer library provides hardware-agnostic meshlet generation and culling utilities for GPU-driven rendering pipelines. When rendering large meshes using meshlets (clusters of triangles), **cluster cone culling** allows you to reject entire meshlets when their triangle normals point away from the camera, eliminating redundant GPU work. This technique relies on `meshopt_computeMeshletBounds` to generate cone data that describes the dominant normal direction of each meshlet as implemented in `zeux/meshoptimizer`.

## Understanding the Cone Data Structure

In [`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp), the internal `computeClusterBounds` function (lines 89-115) constructs a normal cone from triangle normals. This data is returned in the `meshopt_Bounds` structure defined in [`include/meshoptimizer.h`](https://github.com/zeux/meshoptimizer/blob/main/include/meshoptimizer.h):

```cpp
struct meshopt_Bounds {
    float center[3];
    float radius;
    float cone_apex[3];
    float cone_axis[3];
    float cone_cutoff;        // cos(θ/2) where θ is the cone angle
    signed char cone_axis_s8[3];
    signed char cone_cutoff_s8;
};

```

The **cone apex** represents a point in the cluster, the **cone axis** is the average normal direction, and the **cone cutoff** stores the cosine of half the cone angle. When the cone is degenerate (wider than approximately 168°), the function sets `cone_cutoff = 1`, forcing the culling test to always fail and keeping the meshlet visible.

## Step 1 — Generate Meshlets with Cone Weight

To generate cone data during meshlet creation, call `meshopt_buildMeshlets` with a non-zero `cone_weight`. According to the implementation in [`src/clusterizer.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/clusterizer.cpp) (line 173), this parameter biases the meshlet construction algorithm toward tighter normal cones.

```cpp
size_t maxMeshlets = meshopt_buildMeshletsBound(indexCount, maxVertices, maxTriangles);
std::vector<meshopt_Meshlet> meshlets(maxMeshlets);
std::vector<unsigned int> meshlet_vertices(maxMeshlets * maxVertices);
std::vector<unsigned char> meshlet_triangles(maxMeshlets * maxTriangles * 3);

size_t meshletCount = meshopt_buildMeshlets(
    meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(),
    indices, indexCount,
    positions, vertexCount, sizeof(float) * 3,
    maxVertices, maxTriangles, /* cone_weight = */ 0.5f);

```

## Step 2 — Compute Per-Meshlet Bounds

After generating meshlets, compute bounds for each entry using `meshopt_computeMeshletBounds`. This function, located at line 311 in [`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp), wraps the internal `computeClusterBounds` logic and returns the `meshopt_Bounds` structure containing the cone data.

```cpp
std::vector<meshopt_Bounds> bounds(meshletCount);
for (size_t i = 0; i < meshletCount; ++i) {
    const meshopt_Meshlet& m = meshlets[i];
    bounds[i] = meshopt_computeMeshletBounds(
        &meshlet_vertices[m.vertex_offset],
        &meshlet_triangles[m.triangle_offset],
        m.triangle_count,
        positions, vertexCount, sizeof(float) * 3);
}

```

## Step 3 — Implement the Cone Culling Test

At render time, perform the culling test by checking if the view direction points away from the cone. If the dot product between the normalized vector from cone apex to camera and the cone axis is less than `cone_cutoff`, the meshlet is invisible.

```cpp
bool coneCull(const meshopt_Bounds& b, const float camPos[3]) {
    // Vector from cone apex to camera
    float v[3] = {
        camPos[0] - b.cone_apex[0],
        camPos[1] - b.cone_apex[1],
        camPos[2] - b.cone_apex[2]
    };
    
    // Normalize
    float len = sqrtf(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);
    if (len == 0.0f) return false;
    
    v[0] /= len; v[1] /= len; v[2] /= len;
    
    // Dot product with cone axis
    float dot = v[0]*b.cone_axis[0] + v[1]*b.cone_axis[1] + v[2]*b.cone_axis[2];
    return dot < b.cone_cutoff;  // True if invisible
}

```

If `coneCull` returns **true**, skip rendering the meshlet.

## Combining with Sphere Culling

For tighter culling, combine the cone test with the bounding sphere data stored in `meshopt_Bounds.center` and `meshopt_Bounds.radius`. The demo implementation in [`demo/clusterlod.h`](https://github.com/zeux/meshoptimizer/blob/main/demo/clusterlod.h) (line 228) demonstrates this combined approach. Testing the sphere first provides a cheap early-out: if the camera is outside the sphere and the cone test fails, you can safely cull the meshlet.

## Source File References

The cone generation logic is implemented across these files in the `zeux/meshoptimizer` repository:

- **[`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp)** (lines 75, 311): Contains `computeClusterBounds` and the public `meshopt_computeMeshletBounds` function that builds the cone from triangle normals.
- **[`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp)** (lines 89-115): Core cone construction logic that computes the apex, axis, and cutoff from cluster triangles.
- **[`src/clusterizer.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/clusterizer.cpp)** (line 173): Meshlet generation code that uses `cone_weight` to bias clustering toward tighter normal cones.
- **[`include/meshoptimizer.h`](https://github.com/zeux/meshoptimizer/blob/main/include/meshoptimizer.h)**: Public API declarations and the `meshopt_Bounds` structure definition.
- **[`demo/clusterlod.h`](https://github.com/zeux/meshoptimizer/blob/main/demo/clusterlod.h)** (line 228): Example usage of bounds data for LOD selection and culling.

## Summary

- **`meshopt_computeMeshletBounds`** generates cone data (apex, axis, cutoff) from meshlet geometry in [`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp).
- Enable cone generation during meshlet construction by passing a non-zero `cone_weight` (e.g., `0.5f`) to `meshopt_buildMeshlets`.
- The cone culling test rejects meshlets when `dot(normalize(apex - camera), axis) < cone_cutoff`.
- Degenerate cones (wider than ~168°) have `cone_cutoff = 1` and cannot be culled.
- Combine cone tests with bounding sphere checks for optimal early-Z rejection.

## Frequently Asked Questions

### What is the difference between meshopt_computeMeshletBounds and meshopt_computeClusterBounds?

`meshopt_computeClusterBounds` computes bounds for any arbitrary set of triangles defined by an index buffer, while `meshopt_computeMeshletBounds` is specifically optimized for meshlets generated by `meshopt_buildMeshlets`. Both functions ultimately call the internal `computeClusterBounds` implementation in [`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp) (line 75) and return the same `meshopt_Bounds` structure containing cone data.

### Why does cone culling fail when the cone cutoff equals 1.0?

When the normals within a meshlet vary by more than approximately 168 degrees, the algorithm cannot construct a meaningful cone that encompasses all normals. In this degenerate case, `meshopt_computeClusterBounds` sets `cone_cutoff = 1.0` (lines 89-115 in [`src/meshletutils.cpp`](https://github.com/zeux/meshoptimizer/blob/main/src/meshletutils.cpp)), causing the dot product test to never satisfy the cull condition, ensuring the meshlet remains visible from all view directions.

### Can cone culling be performed on the GPU?

Yes. The `meshopt_Bounds` structure provides quantized 8-bit values (`cone_axis_s8` and `cone_cutoff_s8`) specifically for GPU consumption. You can upload these bounds to a GPU buffer and perform the same dot product test in a compute shader or mesh shader to cull meshlets before rasterization, using the quantized values to reduce memory bandwidth.

### What cone weight value should I use with meshopt_buildMeshlets?

Use a value between `0.0f` and `1.0f` depending on your geometry. A value of `0.5f` provides a balanced trade-off between meshlet compactness and cone tightness. Higher values prioritize tighter normal cones (better culling) but may produce less optimal vertex reuse, while `0.0f` disables cone-aware clustering entirely.