How meshopt_generateShadowIndexBuffer Works for Shadow Passes
meshopt_generateShadowIndexBuffer generates a remapped index buffer that collapses vertices sharing identical position data, eliminating redundant vertex fetches during depth-only shadow map rendering.
The meshoptimizer library by Zeux provides algorithms for preparing mesh data for efficient GPU consumption. When rendering shadow maps or depth pre-passes, the GPU only requires vertex position data—attributes like normals, tangents, and texture coordinates are discarded by the rasterizer. The meshopt_generateShadowIndexBuffer function in src/indexgenerator.cpp creates a specialized shadow index buffer that treats vertices differing only in discarded attributes as duplicates, significantly reducing memory bandwidth and improving cache utilization during shadow pass rendering.
What is a Shadow Index Buffer?
A shadow index buffer is a secondary index list derived from your mesh's original index buffer. Unlike the primary buffer used for full shading passes, this variant indexes into vertex data using only the components relevant to depth calculation—typically just the position. Vertices that occupy the same spatial coordinates but have differing normals or UVs (common at hard edges and texture seams) map to a single shared index in the shadow buffer. This deduplication reduces the number of unique vertices the GPU must fetch and process when performing depth-only rasterization.
This optimization deliberately occurs after other mesh optimizations such as vertex cache optimization and vertex compression because it operates on the final vertex layout. The README's "Shadow indexing" section notes that the benefit is greatest when source meshes contain many attribute seams.
Three-Stage Generation Algorithm
The implementation in src/indexgenerator.cpp processes input data through three distinct stages to produce the optimized buffer, utilizing template-based hashing for performance.
Vertex Hashing with VertexHasher
The algorithm begins by constructing a VertexHasher instance that treats specific byte ranges of each vertex as the comparison key. In the single-stream version, this key consists of the bytes comprising the position attribute starting at the specified offset. The hasher processes raw vertex memory using the provided vertex size and stride parameters.
The construction appears at lines 30-33 of src/indexgenerator.cpp, where the hasher is initialized with the vertex buffer pointer, count, size, and stride:
VertexHasher hasher(static_cast<const unsigned char*>(vertices),
vertex_count,
vertex_size,
vertex_stride);
Remap Table Construction
The internal generateShadowBuffer function walks the original index buffer and builds a remap table mapping original vertex indices to canonical indices. For each input index, the function either creates a new entry in the hash table (for first occurrences) or retrieves the previously stored canonical index (for duplicates). The destination buffer receives these remapped indices, producing an index list where position-identical vertices share the same reference.
This core logic appears in lines 302-329 of src/indexgenerator.cpp. The function asserts that the input index count is a multiple of three (enforcing triangle list topology) and validates that vertex size constraints fit within the provided stride.
Optional Vertex Cache Optimization
After generation, the shadow buffer typically requires re-optimization for vertex cache locality. Because the remapping process changes index ordering based on hash table traversal rather than spatial locality, you should pass the new buffer to meshopt_optimizeVertexCache before GPU submission. This rearranges the triangle order to maximize post-transform cache hits while maintaining the deduplicated index structure.
Single-Stream vs Multi-Stream APIs
The library provides two variants for different vertex layout scenarios.
meshopt_generateShadowIndexBuffer expects a contiguous vertex buffer where position data starts at the specified pointer offset within each vertex structure. This suits the common case where positions are interleaved with other attributes in a unified array.
meshopt_generateShadowIndexBufferMulti accepts an array of meshopt_Stream structures, allowing the hash key to be built from non-contiguous data sources. For example, you might reference positions from one buffer and skinning weights from another to generate a shadow buffer for animated mesh rendering.
Both functions validate that the index count divides evenly by three and that stride parameters accommodate the vertex size.
Implementation Details and Source References
The shadow buffer generation centers on specific locations in src/indexgenerator.cpp:
- Lines 30-33: The
VertexHasherconstructor initializes the hashing infrastructure for the specified vertex memory layout. - Lines 302-329: The
generateShadowBuffertemplate performs the actual index remapping by iterating input indices and consulting the hash table for canonical equivalents.
The public API declarations reside in src/meshoptimizer.h, providing C-compatible wrapper functions for both single-stream and multi-stream variants. Practical implementation examples appear in demo/main.cpp at approximately lines 910-917 and 1794-1795, demonstrating integration with vertex cache optimization pipelines.
Practical Usage Example
The following example demonstrates generating a shadow buffer for a mesh with interleaved vertices (position followed by other attributes), followed by vertex cache optimization:
#include "meshoptimizer.h"
#include <vector>
// Assuming mesh.indices and mesh.vertices are populated
std::vector<unsigned int> shadow_indices(mesh.indices.size());
// Generate shadow buffer using position-only data (float3 at start of vertex)
meshopt_generateShadowIndexBuffer(
shadow_indices.data(),
mesh.indices.data(),
mesh.indices.size(),
&mesh.vertices[0].px, // Pointer to first position component
mesh.vertices.size(),
sizeof(float) * 3, // Size of position key (12 bytes)
sizeof(Vertex) // Full vertex stride
);
// Optimize the new buffer for vertex cache locality
meshopt_optimizeVertexCache(
shadow_indices.data(),
shadow_indices.data(),
shadow_indices.size(),
mesh.vertices.size()
);
For multi-stream scenarios where positions reside in a separate buffer from other attributes:
meshopt_Stream streams[1];
streams[0].data = position_buffer;
streams[0].size = vertex_count * sizeof(float) * 3;
streams[0].stride = sizeof(float) * 3;
streams[0].offset = 0;
std::vector<unsigned int> shadow_indices(index_count);
meshopt_generateShadowIndexBufferMulti(
shadow_indices.data(),
indices.data(),
index_count,
vertex_count,
streams,
1 // Stream count
);
Summary
- meshopt_generateShadowIndexBuffer generates a deduplicated index buffer for depth-only rendering by hashing vertex positions in
src/indexgenerator.cpp. - The algorithm uses
VertexHasher(lines 30-33) andgenerateShadowBuffer(lines 302-329) to create a remap table that collapses position-identical vertices. - Two API variants exist: single-stream for interleaved vertex data and multi-stream for split attribute buffers.
- Always follow shadow buffer generation with
meshopt_optimizeVertexCacheto restore GPU cache locality before rendering. - The function requires triangle lists (index count divisible by three) and precise pointer arithmetic based on vertex stride and size parameters.
Frequently Asked Questions
When should I use meshopt_generateShadowIndexBuffer?
Use this function when your mesh contains attribute seams—vertices split by differing normals, UVs, or colors that share identical positions. Shadow maps and depth pre-passes only test depth, so these splits create redundant vertex processing. The function eliminates this overhead by collapsing position duplicates into single indices, particularly beneficial for high-poly meshes with many hard edges or UV boundaries. According to the meshoptimizer source code, the benefit is greatest when the source mesh contains many such attribute seams.
How does the multi-stream variant differ from the single-stream version?
meshopt_generateShadowIndexBufferMulti accepts an array of meshopt_Stream structures, each defining a separate memory region with its own stride and offset. This allows the hash key to incorporate data from multiple buffers—for example, combining base positions from one stream with morph targets or skinning data from another. The single-stream version assumes all relevant bytes reside within one contiguous vertex structure starting at the specified pointer, which is the most common layout for static meshes.
Why must I optimize the vertex cache after generating a shadow buffer?
The remapping process reorders indices based on hash table traversal rather than spatial locality. While this eliminates duplicate vertex references, it typically destroys the cache-friendly triangle ordering achieved by previous optimizations. Passing the shadow buffer through meshopt_optimizeVertexCache (as demonstrated in demo/main.cpp lines 910-917) restores post-transform cache hit rates without reintroducing vertex duplicates.
Can I use the shadow buffer for anything other than shadow rendering?
While designed for depth-only passes, the buffer works for any rasterization pass that discards vertex attributes and relies solely on the data used to construct the hash key. This includes occlusion culling passes, z-prepass rendering, and some types of collision mesh visualization. However, do not use it for standard shaded rendering where normals, colors, or UVs affect the output, as the deduplication would incorrectly merge distinct surface properties that share only positional data.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →