How to Use meshopt_encodeMeshlet for Mesh Shader Workloads

The meshopt_encodeMeshlet function in the zeux/meshoptimizer library packs meshlet vertex and triangle data into a compact binary format that can be streamed directly to GPU mesh shaders, following a three-stage workflow of building meshlets, optimizing their layout, and encoding them for GPU consumption.

The zeux/meshoptimizer repository provides a production-ready toolkit for converting traditional indexed geometry into meshlet structures optimized for modern GPU mesh shader pipelines. By leveraging meshopt_encodeMeshlet, developers can generate compact, self-contained binary blobs that minimize CPU overhead and enable efficient GPU-side decoding.

The Three-Stage Meshlet Pipeline

Creating mesh shader ready geometry requires three distinct phases: partitioning the mesh into meshlets, optimizing their internal layout for locality and compression, and finally encoding them into a GPU-friendly binary format.

Stage 1: Building Meshlets with meshopt_buildMeshlets

The process begins with meshopt_buildMeshlets, declared in src/meshoptimizer.h, which splits a classic index buffer into discrete meshlets respecting hardware limits. For mesh shader workloads, constrain max_vertices to ≤ 256 and max_triangles to ≤ 512 to ensure compatibility with typical GPU mesh shader capabilities.

Always compute the worst-case allocation size using meshopt_buildMeshletsBound before invoking the builder:

size_t maxMeshlets = meshopt_buildMeshletsBound(
    indexCount,
    64,    // max_vertices per meshlet
    128);  // max_triangles per meshlet

The function populates meshopt_Meshlet structures containing vertex offsets, counts, and triangle offsets that describe each meshlet's bounds within the global vertex and index arrays.

Stage 2: Optimizing Layout with meshopt_optimizeMeshletLevel

After building, optimize each meshlet's internal layout using meshopt_optimizeMeshletLevel, also declared in src/meshoptimizer.h. This function reorders vertex and triangle indices to improve cache locality and compression ratios:

  • Level 0: Equivalent to meshopt_optimizeMeshlet (basic optimization)
  • Levels 1-9: Progressively rotate triangle corners for better compression

For most workloads, level 3 provides the optimal balance between compression density and processing time.

Stage 3: Encoding with meshopt_encodeMeshlet

The final stage converts the optimized meshlet into a compact binary format using meshopt_encodeMeshlet. Before encoding, allocate a destination buffer sized using meshopt_encodeMeshletBound(max_vertices, max_triangle_count).

Critical constraints for encoding:

  • vertex_count and triangle_count must be ≤ 256
  • The vertices parameter may be NULL when only triangle data requires encoding

The encoder returns the actual number of bytes written, or 0 on error. As implemented in src/meshoptimizer.cpp, the encoder is safe for untrusted input and never writes past the supplied buffer boundaries.

size_t bound = meshopt_encodeMeshletBound(meshlet.vertex_count, meshlet.triangle_count);
std::vector<unsigned char> buffer(bound);

size_t encodedSize = meshopt_encodeMeshlet(
    buffer.data(),
    bound,
    meshletVertices.data() + meshlet.vertex_offset,
    meshlet.vertex_count,
    meshletTriangles.data() + meshlet.triangle_offset,
    meshlet.triangle_count);

if (encodedSize == 0) {
    // Handle encoding error
}

Decoding in Mesh Shaders

At draw time, the mesh shader decodes the blob using meshopt_decodeMeshlet or the SIMD-friendly meshopt_decodeMeshletRaw variant. The decoder reconstructs the vertex index array and packed triangle indices directly within the shader execution context:

unsigned int vertices[256];
unsigned char triangles[512 * 3];

int result = meshopt_decodeMeshlet(
    vertices,           // Destination vertex indices
    256,                // Vertex count
    4,                  // Vertex size in bytes (2 or 4)
    triangles,          // Destination triangle indices
    128,                // Triangle count
    4,                  // Triangle size (typically 3 or 4)
    encodedBlob,        // Source buffer from GPU memory
    blobSize);

// result == 0 indicates successful decoding

Complete Implementation Example

The following implementation demonstrates the full CPU-side workflow from src/meshoptimizer.h through final GPU upload:

// 1. Build meshlets
std::vector<meshopt_Meshlet> meshlets;
std::vector<unsigned int> meshletVertices;
std::vector<unsigned char> meshletTriangles;

size_t maxMeshlets = meshopt_buildMeshletsBound(indexCount, 64, 128);
meshlets.resize(maxMeshlets);
meshletVertices.resize(indexCount);
meshletTriangles.resize(indexCount + maxMeshlets * 3);

size_t meshletCount = meshopt_buildMeshlets(
    meshlets.data(),
    meshletVertices.data(),
    meshletTriangles.data(),
    indices,
    indexCount,
    vertexPositions,
    vertexCount,
    sizeof(float) * 3,
    64, 128, 0.0f);

// 2. Optimize each meshlet
for (size_t i = 0; i < meshletCount; ++i) {
    const meshopt_Meshlet& m = meshlets[i];
    meshopt_optimizeMeshletLevel(
        meshletVertices.data() + m.vertex_offset,
        m.vertex_count,
        meshletTriangles.data() + m.triangle_offset,
        m.triangle_count,
        3);  // Recommended compression level
}

// 3. Encode each meshlet
std::vector<std::vector<unsigned char>> encodedMeshlets;
for (size_t i = 0; i < meshletCount; ++i) {
    const meshopt_Meshlet& m = meshlets[i];
    size_t bound = meshopt_encodeMeshletBound(m.vertex_count, m.triangle_count);
    std::vector<unsigned char> buf(bound);
    
    size_t size = meshopt_encodeMeshlet(
        buf.data(), bound,
        meshletVertices.data() + m.vertex_offset, m.vertex_count,
        meshletTriangles.data() + m.triangle_offset, m.triangle_count);
    
    buf.resize(size);
    encodedMeshlets.push_back(std::move(buf));
}

// 4. Upload to GPU (pseudo-code)
size_t totalSize = 0;
for (auto& e : encodedMeshlets) totalSize += e.size();
// Allocate GPU buffer and copy concatenated meshlet data

Reference implementations in demo/clusterlod.h demonstrate advanced usage including LOD generation, while gltf/gltfpack.h shows integration with glTF asset pipelines for storing meshlet-encoded data within standard file formats.

Summary

  • meshopt_encodeMeshlet packs meshlet data into a compact binary format suitable for GPU mesh shaders, declared in src/meshoptimizer.h.
  • The workflow requires three stages: building meshlets with meshopt_buildMeshlets, optimizing with meshopt_optimizeMeshletLevel, and encoding with meshopt_encodeMeshlet.
  • Hardware constraints limit encoded meshlets to 256 vertices and 256 triangles per meshlet.
  • Always pre-allocate buffers using meshopt_encodeMeshletBound to ensure sufficient space for the encoded output.
  • The encoder and decoder are safe for untrusted input, returning 0 on error and preventing buffer overruns.
  • Decoding occurs GPU-side via meshopt_decodeMeshlet or meshopt_decodeMeshletRaw within the mesh shader execution context.

Frequently Asked Questions

What are the hardware limits for meshopt_encodeMeshlet?

The meshopt_encodeMeshlet function requires that both vertex_count and triangle_count be less than or equal to 256. While meshopt_buildMeshlets can generate meshlets with up to 512 triangles, you must split larger meshlets or adjust build parameters to meet the encoder's stricter 256-triangle limit for mesh shader compatibility.

How do I calculate the required buffer size for encoding?

Call meshopt_encodeMeshletBound(max_vertices, max_triangles) before encoding to determine the worst-case byte count. This function, defined in src/meshoptimizer.h, returns the maximum possible size for the given meshlet parameters, ensuring your destination buffer is adequately sized before calling meshopt_encodeMeshlet.

Can I encode meshlets without vertex indices?

Yes. Pass NULL for the vertices parameter to meshopt_encodeMeshlet when you only need to encode triangle data. This is useful when vertex data is deduplicated elsewhere or when meshlets share a global vertex buffer, allowing the encoded blob to contain only the triangle index information.

Is meshopt_encodeMeshlet safe for user-generated content?

Yes. According to the implementation in src/meshoptimizer.cpp, the encoder validates input bounds and returns 0 on any error without writing past buffer boundaries. This safety guarantee makes the function suitable for streaming meshlet data from network sources or processing user-generated assets without risking memory corruption.

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 →