How to Encode and Decode Vertex Buffers with Meshoptimizer Compression Codec
Meshoptimizer provides a fast, lossless compression codec that converts raw vertex data into compact byte streams using delta encoding and SIMD acceleration, accessible via meshopt_encodeVertexBuffer and meshopt_decodeVertexBuffer defined in src/meshoptimizer.h.
The zeux/meshoptimizer library implements a specialized vertex compression codec designed for real-time graphics pipelines. This codec reduces vertex buffer size through block-based delta encoding and per-channel analysis while guaranteeing bit-exact reconstruction. All public API functions are declared in src/meshoptimizer.h with the implementation residing in src/vertexcodec.cpp.
Encoding Vertex Buffers
The primary entry point for compression is meshopt_encodeVertexBuffer, which processes raw vertex data and outputs a compact byte stream. For fine-grained control over compression ratios, use meshopt_encodeVertexBufferLevel to specify trade-offs between encoding speed and output size.
Compression Levels and Versioning
The codec supports format versioning via meshopt_encodeVertexVersion(int version), where version 0 indicates legacy format and version 1 (default) enables per-channel control bytes. The level parameter accepts values from 0 to 3:
- Level 0: Fastest encoding, minimal analysis
- Level 3: Maximum compression, full channel analysis
When level is 2 or higher, the encoder invokes estimateRotate and estimateChannel in src/vertexcodec.cpp to analyze vertex data for rotational or predictive patterns, improving compression efficiency.
Block-Based Encoding Process
In src/vertexcodec.cpp, the encoder processes vertices in blocks sized by vertex_block_size to maximize SIMD throughput. For each block, the implementation performs the following steps:
- Writes a header byte combining
kVertexHeaderwith the format version - Stores per-channel control bytes (version 1 only) that specify encoding strategies: literal, zero, or delta-compressed
- Applies delta encoding to store only differences between consecutive vertices
- Quantizes deltas using variable-length byte coding via
encodeBytesSimd - Interleaves bytes for cache-friendly output
After processing all blocks, the encoder writes a tail section containing the first vertex (required for delta reconstruction) and the channel control bytes. The function returns the total encoded size in bytes, or 0 if the output buffer is insufficient.
Decoding Vertex Buffers
Reconstruction uses meshopt_decodeVertexBuffer, which reverses the encoding process safely. The decoder first reads the header byte to determine the format version, then extracts per-channel control bytes when present.
Block-Based Decompression
For each 4-byte channel, the implementation calls decodeVertexBlockSimd in src/vertexcodec.cpp. This routine:
- Interprets control bytes to select between literal, zero, or compressed data paths
- Decompresses the byte stream using
decodeBytesSimd - Reconstructs original values via
decodeDeltas4Simdto reverse delta encoding - Applies the first vertex from the tail section to restore absolute positions
The function returns 0 on success or a non-zero error code if the input data is malformed or truncated.
Safety and Threading
The decoder is safe for untrusted input; it performs bounds checking and will never write outside the destination buffer. However, malformed data may produce garbage vertices rather than crashing the application. Note that encoding functions are not thread-safe when called simultaneously on shared buffers; use separate output buffers per thread or serialize access to meshopt_encodeVertexBuffer.
Complete Code Example
The following example demonstrates encoding and decoding a vertex buffer containing 1000 vertices with 32 bytes per vertex (e.g., position, normal, and texture coordinate data):
#include "meshoptimizer.h"
#include <vector>
#include <cstdio>
// Encode a vertex buffer
size_t encodeVertices(const void* srcVertices,
size_t vertexCount,
size_t vertexSize,
std::vector<unsigned char>& outEncoded)
{
// Allocate worst-case buffer
size_t maxSize = meshopt_encodeVertexBufferBound(vertexCount, vertexSize);
outEncoded.resize(maxSize);
// Encode with default level (2) and default version (1)
size_t encodedSize = meshopt_encodeVertexBuffer(
outEncoded.data(), maxSize,
srcVertices, vertexCount, vertexSize);
if (encodedSize == 0) {
fprintf(stderr, "Encoding failed: output buffer too small\n");
outEncoded.clear();
return 0;
}
outEncoded.resize(encodedSize); // shrink to actual size
return encodedSize;
}
// Decode a vertex buffer
bool decodeVertices(const std::vector<unsigned char>& encoded,
void* dstVertices,
size_t vertexCount,
size_t vertexSize)
{
int err = meshopt_decodeVertexBuffer(
dstVertices, vertexCount, vertexSize,
encoded.data(), encoded.size());
if (err != 0) {
fprintf(stderr, "Decoding error %d\n", err);
return false;
}
return true;
}
// Usage example
int main()
{
const size_t vertexCount = 1000;
const size_t vertexSize = 32; // 8 floats: pos(3) + normal(3) + uv(2)
std::vector<float> vertices(vertexCount * vertexSize / sizeof(float));
// ... fill vertices with data ...
// Encode
std::vector<unsigned char> encoded;
encodeVertices(vertices.data(), vertexCount, vertexSize, encoded);
printf("Encoded %zu bytes (original %zu bytes)\n",
encoded.size(), vertexCount * vertexSize);
// Decode
std::vector<float> decoded(vertexCount * vertexSize / sizeof(float));
if (decodeVertices(encoded, decoded.data(), vertexCount, vertexSize))
printf("Decoding succeeded!\n");
// decoded now matches the original `vertices` bit-exactly
return 0;
}
Summary
- The meshoptimizer vertex codec provides lossless compression via
meshopt_encodeVertexBufferandmeshopt_decodeVertexBuffer - Primary implementation resides in
src/meshoptimizer.h(API) andsrc/vertexcodec.cpp(logic) - Encoding uses block-based delta compression with optional channel analysis at compression levels ≥ 2
- Version 1 format includes per-channel control bytes that improve compression ratios over legacy version 0
- The decoder is safe for untrusted input, but encoding requires application-level thread synchronization
Frequently Asked Questions
What is the difference between meshopt_encodeVertexBuffer and meshopt_encodeVertexBufferLevel?
meshopt_encodeVertexBuffer uses a default compression level of 2, providing a balance between speed and ratio. meshopt_encodeVertexBufferLevel accepts an explicit level parameter (0-3) where level 0 prioritizes encoding speed and level 3 maximizes compression by enabling full channel analysis via estimateRotate and estimateChannel.
Is the meshoptimizer vertex codec lossless?
Yes, the compression is lossless and bit-exact. The delta encoding and quantization process preserves all original vertex data, ensuring that decoded vertices match the source data exactly.
Is the vertex codec safe for untrusted input?
The decoder is safe for untrusted input; it performs bounds checking and will not write outside the destination buffer even with malformed data. However, malformed data may produce garbage vertices. The encoder is not safe for concurrent access without external synchronization.
Can I encode vertex buffers from multiple threads simultaneously?
You must synchronize access or use separate output buffers per thread. The encoding functions in src/vertexcodec.cpp are not thread-safe when writing to shared buffers simultaneously, though the decoding functions can be called concurrently on different input/output buffers.
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 →