How to Generate Shadow Index Buffers with meshopt_generateShadowIndexBuffer for Depth-Only Passes

Use meshopt_generateShadowIndexBuffer to create a position-only index buffer that eliminates duplicate vertices differing only in irrelevant attributes, reducing vertex fetch bandwidth during shadow map and Z-prepass rendering.

The meshoptimizer library by zeux provides specialized tools for optimizing mesh data for GPU consumption. When rendering depth-only passes such as shadow maps or Z-prepasses, vertex attributes like normals, texture coordinates, and skinning data are unused, yet the GPU may still fetch these redundant values. Generating a shadow index buffer allows you to re-index geometry based solely on vertex positions, minimizing unnecessary memory bandwidth and improving cache locality.

Understanding Shadow Index Buffers

A standard index buffer may reference multiple vertices that share identical positions but differ in other attributes. During depth-only rendering, the GPU wastes cycles fetching complete vertex structures when only the position matters. Shadow index buffers solve this by creating a remapped index list that references only unique position data, effectively deduplicating the vertex stream for position-only passes.

Algorithm Implementation in meshoptimizer

The implementation in src/indexgenerator.cpp uses a hash-based approach to identify unique vertex positions efficiently.

Vertex Hashing Strategy

The algorithm constructs a hash table keyed by the raw bytes of vertex data. In src/indexgenerator.cpp lines 22-34, the VertexHasher struct processes vertex_size bytes at vertex_stride offsets using a Murmur-style hash routine (hashUpdate4). This stores the first occurrence of each distinct vertex position, ensuring O(1) lookup time for duplicate detection.

Index Remapping Process

For every index in the original buffer, the function looks up the vertex in the hash table and writes the canonical index (the first occurrence of that position) into the destination buffer. The result is a new index buffer that references only the unique set of positions, leaving the original vertex buffer unmodified but producing a compact index list optimal for depth-only passes.

Function Signatures and Constraints

The API declares two variants in src/meshoptimizer.h:

  • meshopt_generateShadowIndexBuffer – Single vertex stream version
  • meshopt_generateShadowIndexBufferMulti – Multi-stream variant for complex vertex layouts

Critical constraints for the single-stream version:

Parameter Requirement
index_count Must be a multiple of 3 (triangular mesh only)
vertex_size 0 < size ≤ 256 and must be ≤ vertex_stride
vertex_stride Byte distance between consecutive vertices in the buffer
vertices Pointer to the start of the position attribute (usually the first field)

The function performs no vertex buffer modification; it only generates a new index list compatible with the existing vertex data.

Basic Usage Example

The following pattern from demo/main.cpp lines 909-915 demonstrates standard usage:

// Assume mesh contains positions at offset 0
std::vector<unsigned int> shadowIndices(mesh.indices.size());

meshopt_generateShadowIndexBuffer(
    &shadowIndices[0],                 // destination buffer
    &mesh.indices[0],                   // original indices
    mesh.indices.size(),                // index count (multiple of 3)
    &mesh.vertices[0].px,               // pointer to position attribute
    mesh.vertices.size(),               // vertex count
    sizeof(float) * 3,                  // vertex_size (3 floats)
    sizeof(Vertex)                     // vertex_stride (full struct size)
);

// Optional: optimize vertex cache for the shadow pass
meshopt_optimizeVertexCache(
    &shadowIndices[0],
    &shadowIndices[0],
    shadowIndices.size(),
    mesh.vertices.size()
);

This example generates a shadow buffer from positions stored as the first field of a vertex structure, then applies vertex cache optimization to improve GPU locality during the depth pass.

Multi-Stream Vertex Data

For engines using multi-stream vertex formats (position in buffer 0, other attributes elsewhere), use meshopt_generateShadowIndexBufferMulti as implemented in src/indexgenerator.cpp lines 36-48:

meshopt_Stream streams[] = {
    { &positions[0], sizeof(float) * 3, sizeof(float) * 3 }, // positions only
    // Additional streams can be added for alpha-tested geometry requiring UVs
};

std::vector<unsigned int> shadowIndices(totalIndices);

meshopt_generateShadowIndexBufferMulti(
    &shadowIndices[0],
    &indices[0],
    totalIndices,
    totalVertices,
    streams,
    1 // number of streams to consider for hashing
);

This variant allows you to specify exactly which vertex streams contain relevant data for the depth pass, supporting complex asset pipelines where positions reside in separate buffers from vertex attributes.

Post-Processing Recommendations

Because the shadow buffer preserves the original vertex order initially, always run meshopt_optimizeVertexCache on the generated indices before use. According to the library documentation in README.md lines 165-176, this step maximizes vertex cache hit rates during depth-only rendering, compounding the bandwidth savings from the shadow buffer generation.

Summary

  • Shadow index buffers reduce vertex fetch bandwidth by deduplicating vertices based solely on position data.
  • Hash-based deduplication in src/indexgenerator.cpp identifies unique positions using a Murmur-style hash over raw vertex bytes.
  • Function constraints require triangular input (index count multiple of 3) and proper vertex size/stride parameters.
  • Post-process with meshopt_optimizeVertexCache to ensure optimal GPU vertex cache utilization during depth passes.
  • Multi-stream support via meshopt_generateShadowIndexBufferMulti handles split vertex attribute layouts common in modern engines.

Frequently Asked Questions

What is the difference between a regular index buffer and a shadow index buffer?

A regular index buffer references all vertex attributes, including normals and UVs. A shadow index buffer references only unique positions, allowing the GPU to skip fetching irrelevant attributes during depth-only passes. This reduces memory bandwidth when rendering shadow maps or performing Z-prepasses.

When should I call meshopt_optimizeVertexCache on the shadow buffer?

Call meshopt_optimizeVertexCache immediately after generating the shadow buffer. Because meshopt_generateShadowIndexBuffer preserves the original vertex reference order, the resulting indices may have poor locality. The optimization pass reorders triangles to maximize vertex cache hits during the depth-only render pass.

Can I use meshopt_generateShadowIndexBuffer with non-triangular meshes?

No. The function requires index_count to be a multiple of 3, as it is designed specifically for triangular meshes. Attempting to use line lists or point clouds will produce undefined behavior or incorrect results.

Does the function modify the original vertex buffer?

No. Both meshopt_generateShadowIndexBuffer and meshopt_generateShadowIndexBufferMulti are read-only operations with respect to vertex data. They only write to the destination index buffer you provide, leaving the source vertices and original indices unchanged.

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 →