How to Use Multi-Stream Vertex Remapping with meshopt_generateVertexRemapMulti
Use meshopt_generateVertexRemapMulti to generate a vertex remap table for meshes with multiple attribute streams by describing each channel as a meshopt_Stream structure and invoking the function with your vertex buffers and optional index data.
The meshopt_generateVertexRemapMulti function in the zeux/meshoptimizer library enables multi-stream vertex remapping for complex mesh layouts where attributes like positions, normals, and UVs reside in separate buffers or interleaved memory. Unlike the single-stream meshopt_generateVertexRemap, this routine accepts an array of stream descriptors, treating vertices as duplicates only when every attribute stream matches byte-for-byte.
Understanding the meshopt_Stream Structure
The foundation of multi-stream remapping is the meshopt_Stream structure defined in src/meshoptimizer.h (lines 46‑51). This simple POD describes how to access individual attributes within your vertex data:
data– Pointer to the raw buffer containing the attribute values.size– Size of each element in bytes (e.g., 12 for a 3-float vector).stride– Byte distance between consecutive elements in the buffer.
When passing multiple streams to meshopt_generateVertexRemapMulti, you provide an array of these descriptors—one per attribute channel. The function supports between 1 and 16 streams, allowing you to handle everything from simple position-only meshes to complex layouts with colors, tangents, and bone weights.
Generating the Remap Table
Function Signature and Parameters
According to the implementation in src/indexgenerator.cpp (lines 403‑420), the function signature is:
size_t meshopt_generateVertexRemapMulti(
unsigned int* destination,
const unsigned int* indices,
size_t index_count,
size_t vertex_count,
const meshopt_Stream* streams,
size_t stream_count
);
The parameters operate as follows:
- destination – Pre-allocated array of
unsigned intwithvertex_countentries; receives the old-to-new vertex mapping. - indices – Optional index buffer; pass
NULLfor unindexed meshes. - index_count / vertex_count – Count of indices and maximum vertices.
- streams – Pointer to an array of
meshopt_Streamdescribing each attribute. - stream_count – Number of streams (must be 1‑16).
The function returns the count of unique vertices after deduplication.
Internal Hashing Logic
Behind the scenes, the function instantiates a VertexStreamHasher (defined in src/indexgenerator.cpp, lines 55‑87). This hasher iterates over every supplied stream, computing a hash from the size bytes of each element and comparing vertices element-wise across all streams. Two vertices are considered identical only if all streams match exactly, ensuring attribute fidelity during remapping.
Practical Code Examples
Separate Position and Normal Buffers
When attributes live in distinct arrays, define each stream independently:
// Existing data: float positions[vertex_count*3], normals[vertex_count*3]
// Optional: unsigned int indices[index_count]
meshopt_Stream streams[2];
streams[0].data = positions;
streams[0].size = 12; // 3 floats * 4 bytes
streams[0].stride = 12; // Tightly packed
streams[1].data = normals;
streams[1].size = 12;
streams[1].stride = 12;
unsigned int* remap = (unsigned int*)malloc(vertex_count * sizeof(unsigned int));
size_t unique_vertex_count = meshopt_generateVertexRemapMulti(
remap,
indices, // NULL if unindexed
index_count,
vertex_count,
streams,
2 // Two attribute streams
);
// Allocate new buffers with the reduced size
float* new_positions = (float*)malloc(unique_vertex_count * 3 * sizeof(float));
float* new_normals = (float*)malloc(unique_vertex_count * 3 * sizeof(float));
meshopt_remapVertexBuffer(new_positions, positions, vertex_count, sizeof(float) * 3, remap);
meshopt_remapVertexBuffer(new_normals, normals, vertex_count, sizeof(float) * 3, remap);
Interleaved Vertex Layouts
For interleaved data, calculate offsets into the single buffer while maintaining the same stride for all streams:
// Layout: [position][normal][uv] per vertex (3+3+2 floats = 8 floats total)
float* vertex_data = /* your buffer */;
size_t stride = (3 + 3 + 2) * sizeof(float); // 32 bytes
meshopt_Stream streams[3];
streams[0].data = vertex_data + 0; // Position offset
streams[0].size = 12; // 3 floats
streams[0].stride = stride;
streams[1].data = vertex_data + 3; // Normal offset (after 3 floats)
streams[1].size = 12;
streams[1].stride = stride;
streams[2].data = vertex_data + 6; // UV offset (after 6 floats)
streams[2].size = 8; // 2 floats
streams[2].stride = stride;
unsigned int* remap = (unsigned int*)malloc(vertex_count * sizeof(unsigned int));
size_t unique = meshopt_generateVertexRemapMulti(
remap,
NULL, // Unindexed mesh
0,
vertex_count,
streams,
3
);
// Remap the interleaved buffer
char* new_vertices = (char*)malloc(unique * stride);
meshopt_remapVertexBuffer(new_vertices, vertex_data, vertex_count, stride, remap);
Applying the Remap to Index Buffers
After generating the remap table, update your index buffer to reference the new vertex positions:
unsigned int* new_indices = (unsigned int*)malloc(index_count * sizeof(unsigned int));
meshopt_remapIndexBuffer(new_indices, indices, index_count, remap);
This step is required when working with indexed meshes to ensure triangles point to the deduplicated vertex array.
Summary
meshopt_generateVertexRemapMultiprocesses up to 16 attribute streams viameshopt_Streamdescriptors defined insrc/meshoptimizer.h.- The internal
VertexStreamHasher(insrc/indexgenerator.cpp) treats vertices as duplicates only when all streams match byte-for-byte within their specifiedsize. - The function returns the unique vertex count and populates a remap array suitable for
meshopt_remapVertexBufferandmeshopt_remapIndexBuffer. - Works with both indexed meshes (supply an index buffer) and unindexed meshes (pass
NULLfor indices). - Supports both separate attribute buffers and interleaved layouts by adjusting the
datapointer andstridevalues in each stream descriptor.
Frequently Asked Questions
What is the maximum number of attribute streams supported?
The function accepts between 1 and 16 streams via the stream_count parameter. Exceeding this range violates the API contract as implemented in src/indexgenerator.cpp.
Can I use this function with unindexed mesh data?
Yes. Pass NULL for the indices parameter and 0 for index_count. The remapping logic operates purely on the vertex streams, generating a compact vertex buffer without requiring triangle indices.
How does the library determine if two vertices are duplicates?
The VertexStreamHasher class performs element-wise binary comparison across all defined streams. For each vertex, it hashes size bytes from every stream; a collision occurs only if all bytes match exactly across every attribute channel.
Is it necessary to remap the index buffer after vertex deduplication?
Yes. After calling meshopt_generateVertexRemapMulti, you must pass the resulting remap table to meshopt_remapIndexBuffer (if using indexed geometry) to update triangle indices to point to the new vertex locations. Failure to do so results in corrupted geometry referencing old vertex indices.
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 →