How to Implement Cluster Cone Culling with meshopt_computeMeshletBounds in meshoptimizer

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, 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:

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 (line 173), this parameter biases the meshlet construction algorithm toward tighter normal cones.

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, wraps the internal computeClusterBounds logic and returns the meshopt_Bounds structure containing the cone data.

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.

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 (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 (lines 75, 311): Contains computeClusterBounds and the public meshopt_computeMeshletBounds function that builds the cone from triangle normals.
  • src/meshletutils.cpp (lines 89-115): Core cone construction logic that computes the apex, axis, and cutoff from cluster triangles.
  • src/clusterizer.cpp (line 173): Meshlet generation code that uses cone_weight to bias clustering toward tighter normal cones.
  • include/meshoptimizer.h: Public API declarations and the meshopt_Bounds structure definition.
  • 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.
  • 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 (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), 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.

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 →