# How to Use meshopt_buildMeshletsSpatial for Ray Tracing with SAH Optimization

> Optimize meshes for ray tracing with meshopt_buildMeshletsSpatial. Learn to cluster triangles, minimize SAH cost, and compute bounds for efficient GPU culling.

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

---

**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`](https://github.com/zeux/meshoptimizer/blob/main/src/meshoptimizer.h) and implemented in [`src/clusterizer.cpp`](https://github.com/zeux/meshoptimizer/blob/main/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`](https://github.com/zeux/meshoptimizer/blob/main/src/meshoptimizer.h) (lines 757–758) is:

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

```cpp
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`:

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

```cpp
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`:

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

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