How to Use meshopt_buildMeshletsSpatial for Ray Tracing with SAH Optimization

Use meshopt_buildMeshletsSpatial from the meshoptimizer library to cluster triangles into meshlets optimized for ray tracing by minimizing Surface Area Heuristic (SAH) cost during subdivision, then compute per-meshlet bounds for efficient GPU culling.

The zeux/meshoptimizer library provides meshopt_buildMeshletsSpatial, a specialized meshlet builder that applies surface-area-heuristic (SAH) optimization to generate clusters ideal for ray-tracing pipelines. Unlike the standard cone-weighted builder, this spatial variant minimizes expected ray-intersection costs by grouping geometrically close triangles together. The function is declared in src/meshoptimizer.h and implemented in src/clusterizer.cpp, offering a direct path to hardware-accelerated mesh shading and custom BVH traversals.

What Is meshopt_buildMeshletsSpatial?

meshopt_buildMeshletsSpatial is the spatial meshlet builder that constructs clusters specifically optimized for ray tracing. While meshopt_optimizeMeshlets (the generic builder) uses cone-weighting to optimize for view frustum culling, the spatial version employs SAH-driven subdivision to reduce ray traversal costs.

The algorithm groups triangles into meshlets by evaluating the surface area of candidate clusters and preferring splits that reduce the SAH cost. This results in spatially coherent clusters that minimize the probability of random ray intersections, leading to fewer traversal steps and better memory coherence on the GPU.

SAH Optimization for Ray Tracing

The surface-area heuristic estimates the probability that a randomly cast ray will intersect a given geometry cluster. By minimizing SAH cost during meshlet construction, you achieve:

  • Fewer ray-cluster intersection tests due to lower traversal depth in acceleration structures.
  • Better load balancing across shader threads because clusters tend to have similar surface areas.
  • Higher bandwidth efficiency from compact meshlets that improve vertex fetch locality.

Function Signature and Parameters

The complete declaration from src/meshoptimizer.h (lines 757–758) is:

size_t meshopt_buildMeshletsSpatial(
    meshopt_Meshlet* meshlets,
    unsigned int*   meshlet_vertices,
    unsigned char*  meshlet_triangles,
    const unsigned int* indices,
    size_t index_count,
    const float*    vertex_positions,
    size_t vertex_count,
    size_t vertex_positions_stride,
    size_t max_vertices,
    size_t min_triangles,
    size_t max_triangles,
    float fill_weight);

Key parameters include:

  • max_vertices and max_triangles: Hardware constraints (typically ≤256 vertices and ≤512 triangles per meshlet).
  • min_triangles: Enforces a minimum cluster size to prevent tiny meshlets.
  • fill_weight: Controls the trade-off between cluster fullness and SAH quality. As documented in src/meshoptimizer.h (lines 555–556), a value of 0.5 is a safe default; larger values bias toward fully-filled meshlets, while smaller values prioritize SAH optimization.

Step-by-Step Implementation Guide

1. Pre-optimize the Index Buffer

Before building spatial meshlets, the input index buffer must be vertex-cache-optimized using meshopt_optimizeVertexCache. This ensures the spatial builder operates on a clean topology, improving both the SAH quality and post-transform cache efficiency.

2. Reserve Memory and Allocate Buffers

Compute the worst-case meshlet count using meshopt_buildMeshletsBound (declared at lines 729–730 in src/meshoptimizer.h):

const size_t max_vertices = 64;
const size_t max_triangles = 128;
const size_t meshlet_bound = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles);

std::vector<meshopt_Meshlet> meshlets(meshlet_bound);
std::vector<unsigned int>    meshlet_vertices(index_count);
std::vector<unsigned char>   meshlet_triangles(index_count);

3. Build Spatial Meshlets with SAH

Call meshopt_buildMeshletsSpatial with your desired constraints and fill_weight:

const size_t min_triangles = 8;
const float fill_weight = 0.5f;

size_t meshlet_count = meshopt_buildMeshletsSpatial(
    meshlets.data(),
    meshlet_vertices.data(),
    meshlet_triangles.data(),
    indices.data(),
    indices.size(),
    positions.data(),
    vertex_count,
    sizeof(float) * 3,  // vertex_positions_stride
    max_vertices,
    min_triangles,
    max_triangles,
    fill_weight);

// Resize buffers to actual usage
meshlets.resize(meshlet_count);

The function returns the number of meshlets written and populates three parallel buffers:

  • meshlets: Array of meshopt_Meshlet structs describing each cluster (offsets and counts).
  • meshlet_vertices: Flat array of unique vertex indices referenced by each meshlet.
  • meshlet_triangles: 8-bit local index buffer (values 0 to vertex_count-1) for triangles within each meshlet.

4. Optional Per-Meshlet Optimization

Reorder vertices and triangles inside each meshlet for better locality using meshopt_optimizeMeshlet (declared at line 666 in src/meshoptimizer.h):

for (size_t i = 0; i < meshlet_count; ++i)
{
    const meshopt_Meshlet& m = meshlets[i];
    meshopt_optimizeMeshlet(
        &meshlet_vertices[m.vertex_offset],
        &meshlet_triangles[m.triangle_offset],
        m.triangle_count,
        m.vertex_count);
}

5. Compute Bounding Volumes for Culling

Generate per-meshlet bounding boxes and cones for frustum and back-face culling using meshopt_computeMeshletBounds:

std::vector<meshopt_Bounds> bounds(meshlet_count);
for (size_t i = 0; i < meshlet_count; ++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.data(),
        vertex_count,
        sizeof(float) * 3);
}

6. Upload to GPU

Transfer the three buffers—meshlet descriptors, vertex indices, and local triangle indices—to GPU memory. Bind them to a mesh shader pipeline or a custom compute kernel for ray-tracing traversal.

Complete Code Example

This example mirrors the reference implementation in demo/main.cpp (line 915):

#include <meshoptimizer/meshoptimizer.h>
#include <vector>
#include <iostream>

int main()
{
    // Input data (already vertex-cache-optimized)
    std::vector<unsigned int> indices = /* ... */;
    std::vector<float>       positions = /* ... */;
    const size_t vertex_count = positions.size() / 3;

    // Reserve space
    const size_t max_vertices = 64;
    const size_t max_triangles = 128;
    const size_t meshlet_bound = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles);

    std::vector<meshopt_Meshlet> meshlets(meshlet_bound);
    std::vector<unsigned int>    meshlet_vertices(indices.size());
    std::vector<unsigned char>   meshlet_triangles(indices.size());

    // Build spatial meshlets with SAH
    const size_t min_triangles = 8;
    const float fill_weight = 0.5f;

    size_t meshlet_count = meshopt_buildMeshletsSpatial(
        meshlets.data(),
        meshlet_vertices.data(),
        meshlet_triangles.data(),
        indices.data(),
        indices.size(),
        positions.data(),
        vertex_count,
        sizeof(float) * 3,
        max_vertices,
        min_triangles,
        max_triangles,
        fill_weight);

    meshlets.resize(meshlet_count);

    // Optional: Optimize each meshlet internally
    for (size_t i = 0; i < meshlet_count; ++i)
    {
        const meshopt_Meshlet& m = meshlets[i];
        meshopt_optimizeMeshlet(
            &meshlet_vertices[m.vertex_offset],
            &meshlet_triangles[m.triangle_offset],
            m.triangle_count,
            m.vertex_count);
    }

    // Compute bounds for ray-tracing culling
    std::vector<meshopt_Bounds> bounds(meshlet_count);
    for (size_t i = 0; i < meshlet_count; ++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.data(),
            vertex_count,
            sizeof(float) * 3);
    }

    std::cout << "Generated " << meshlet_count << " SAH-optimized meshlets.\n";
    // Upload to GPU...
}

Summary

  • meshopt_buildMeshletsSpatial generates ray-tracing-optimized meshlets using SAH-driven subdivision, implemented in src/clusterizer.cpp.
  • The fill_weight parameter (typically 0.5) balances cluster fullness against SAH quality.
  • Always pre-optimize the vertex cache before building spatial meshlets to ensure correct topology processing.
  • The function outputs three buffers—meshlets, meshlet_vertices, and meshlet_triangles—that feed directly into GPU mesh shaders or compute-based ray tracers.
  • Compute per-meshlet bounds with meshopt_computeMeshletBounds for efficient frustum and occlusion culling.

Frequently Asked Questions

What is the difference between meshopt_buildMeshlets and meshopt_buildMeshletsSpatial?

meshopt_buildMeshlets uses cone-weighting to optimize clusters for view frustum culling, making it ideal for rasterization pipelines. meshopt_buildMeshletsSpatial applies surface-area-heuristic (SAH) optimization instead, producing clusters that minimize ray-intersection costs and are therefore superior for ray-tracing applications.

What value should I use for the fill_weight parameter?

A value of 0.5 is recommended as a safe default that balances cluster fullness with SAH quality. Increase this value toward 1.0 if you prefer fully-populated meshlets to reduce memory overhead, or decrease it toward 0.0 to prioritize tighter SAH bounds at the cost of potentially more meshlets.

Do I need to optimize the vertex cache before building spatial meshlets?

Yes. The index buffer should be pre-optimized with meshopt_optimizeVertexCache before calling meshopt_buildMeshletsSpatial. This ensures the spatial builder processes a clean topology, resulting in higher quality clusters and better post-transform cache utilization when the meshlets are rendered.

How do I compute per-meshlet bounding volumes for frustum culling?

Use meshopt_computeMeshletBounds, passing the meshlet's local vertex indices and triangles along with the global vertex positions array. This function returns a meshopt_Bounds struct containing axis-aligned bounding boxes and normal cones that you can upload to the GPU for hardware-accelerated culling during traversal.

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 →