Correct Order for the Mesh Optimization Pipeline in zeux/meshoptimizer
The canonical mesh optimization pipeline in zeux/meshoptimizer follows a strict seven-step sequence: indexing, vertex-cache optimization, optional overdraw optimization, vertex-fetch optimization, vertex quantization, index filtering, and optional shadow indexing. This order is critical because each stage depends on the optimized layout produced by the previous step.
The zeux/meshoptimizer library provides algorithms for preparing geometry for modern GPU rendering pipelines. Applying these operations in the correct sequence—exactly as documented in the repository’s README under Core pipeline (lines 38‑47)—ensures maximum vertex cache utilization, minimal overdraw, and optimal memory bandwidth usage.
Why Pipeline Order Matters
Each optimization step in meshoptimizer assumes the previous stage has produced an optimal index or vertex layout. Later stages depend on earlier stages; for example, vertex-fetch optimization expects a fully indexed mesh, and quantization should occur after all reordering so attribute data is as compact as possible. Reordering steps or skipping prerequisites yields suboptimal performance and can invalidate previous optimizations.
The Seven-Step Mesh Optimization Pipeline
1. Indexing
The first step transforms unindexed triangle soup into an indexed mesh. Create a compact index buffer and eliminate duplicate vertices using meshopt_generateVertexRemap to generate a remap table, then apply it with meshopt_remapIndexBuffer and meshopt_remapVertexBuffer.
This operation is defined in src/meshoptimizer.h and establishes the foundation for all subsequent cache and fetch optimizations.
2. Vertex-Cache Optimization
After indexing, reorder indices to improve post-transform cache hits. The function meshopt_optimizeVertexCache (implemented in src/vcacheoptimizer.cpp) rearranges the index buffer to maximize GPU vertex reuse, reducing the number of vertices processed by the GPU.
3. Overdraw Optimization (Optional)
This optional step rearranges triangles to reduce pixel overdraw. Using meshopt_optimizeOverdraw from src/overdrawoptimizer.cpp, the algorithm reorders indices based on vertex positions to minimize the number of pixels shaded multiple times during forward rendering.
4. Vertex-Fetch Optimization
Once the index order is fixed, reorder the vertex buffer to improve memory bandwidth usage. The function meshopt_optimizeVertexFetchRemap generates an optimal remap, which is then applied via meshopt_remapVertexBuffer and meshopt_remapIndexBuffer. This stage is implemented in src/vfetchoptimizer.cpp.
5. Vertex Quantization
Compress vertex attributes to smaller formats (e.g., half, snorm) using meshopt_encodeVertexBuffer. Quantization must occur after all reordering to ensure the attribute data is in its final compact form. This functionality leverages utilities in src/meshletutils.cpp for advanced encoding.
6. Index Filtering
Remove degenerate or unused indices that may have been introduced during previous steps. The function meshopt_optimizeVertexFetch performs final cleanup on the index buffer, ensuring only valid triangles remain for rendering.
7. Shadow Indexing (Optional)
The final optional step generates a separate index buffer optimized for depth-only passes. meshopt_generateShadowIndices creates a version of the index buffer that is optimal for shadow map rendering, where vertex positions are the only attributes accessed.
Complete Implementation Example
The following C++ implementation demonstrates the exact sequence using the public API from src/meshoptimizer.h:
#include "meshoptimizer.h"
#include <vector>
// Step 1: Indexing – generate remap table and create indexed buffers
std::vector<unsigned int> remap(unindexedVertexCount);
size_t vertexCount = meshopt_generateVertexRemap(
remap.data(), nullptr, indexCount,
unindexedVertices, unindexedVertexCount, sizeof(Vertex));
std::vector<unsigned int> indices(indexCount);
meshopt_remapIndexBuffer(indices.data(), nullptr, indexCount, remap.data());
std::vector<Vertex> vertices(vertexCount);
meshopt_remapVertexBuffer(vertices.data(),
unindexedVertices, unindexedVertexCount, sizeof(Vertex), remap.data());
// Step 2: Vertex-cache optimization
meshopt_optimizeVertexCache(indices.data(), indices.data(),
indexCount, vertexCount);
// Step 3: Overdraw optimization (optional)
meshopt_optimizeOverdraw(indices.data(), indices.data(),
indexCount, &vertices[0].position[0], vertexCount, sizeof(Vertex), 1.05f);
// Step 4: Vertex-fetch optimization
meshopt_optimizeVertexFetchRemap(remap.data(), indices.data(),
indexCount, vertexCount);
meshopt_remapVertexBuffer(vertices.data(), vertices.data(),
vertexCount, sizeof(Vertex), remap.data());
meshopt_remapIndexBuffer(indices.data(), indices.data(),
indexCount, remap.data());
// Step 5: Vertex quantization (example: positions to half-float)
meshopt_encodeVertexBuffer(&vertices[0], vertices.size(),
sizeof(Vertex), meshopt_EncodeVertexBuffer_Float16);
// Step 6: Index filtering (remove degenerate triangles)
meshopt_optimizeVertexFetch(indices.data(), indices.data(),
indexCount, vertices.data(), vertexCount, sizeof(Vertex));
// Step 7: Shadow indexing (optional)
meshopt_generateShadowIndices(indices.data(), indices.data(),
indexCount, vertexCount);
The equivalent JavaScript implementation using the Node.js bindings follows the same logical order:
import {
generateVertexRemap,
remapIndexBuffer,
remapVertexBuffer,
optimizeVertexCache,
optimizeOverdraw,
optimizeVertexFetchRemap,
encodeVertexBuffer,
optimizeVertexFetch,
generateShadowIndices
} from "meshoptimizer";
// Step 1: Indexing
const remap = generateVertexRemap(null, vertexCount);
remapIndexBuffer(indices, null, indexCount, remap);
remapVertexBuffer(vertices, vertexCount, vertexSize, remap);
// Step 2: Vertex-cache optimization
optimizeVertexCache(indices, indexCount, vertexCount);
// Step 3: Overdraw (optional)
optimizeOverdraw(indices, indexCount, vertices, vertexCount, vertexSize, 1.05);
// Step 4: Vertex-fetch optimization
const fetchRemap = optimizeVertexFetchRemap(indices, indexCount, vertexCount);
remapVertexBuffer(vertices, vertexCount, vertexSize, fetchRemap);
remapIndexBuffer(indices, indexCount, fetchRemap);
// Step 5: Quantization (example: positions to half-float)
encodeVertexBuffer(vertices, vertexCount, vertexSize, meshopt.EncodeVertexBuffer_Float16);
// Step 6: Index filtering (removes degenerate tris)
optimizeVertexFetch(indices, indexCount, vertices, vertexCount, vertexSize);
// Step 7: Shadow indexing (optional)
generateShadowIndices(indices, indexCount, vertexCount);
Source Code Architecture
The pipeline implementation is distributed across specific source files in the repository:
src/meshoptimizer.h– Public API header exposing all pipeline functions includingmeshopt_optimizeVertexCacheandmeshopt_generateVertexRemap.src/vcacheoptimizer.cpp– Implements vertex-cache optimizations (meshopt_optimizeVertexCache*).src/overdrawoptimizer.cpp– Implements overdraw reduction (meshopt_optimizeOverdraw).src/vfetchoptimizer.cpp– Implements vertex-fetch reordering and remapping (meshopt_optimizeVertexFetch*).src/meshletutils.cpp– Provides meshlet-level utilities used for advanced quantization and clustering.README.md– Documents the core pipeline order and usage guidelines under the Core pipeline section.
Summary
- Indexing must always occur first to establish the vertex/remap structure required by subsequent steps.
- Vertex-cache optimization (step 2) and overdraw optimization (step 3) modify the index buffer order and must precede vertex buffer layout changes.
- Vertex-fetch optimization (step 4) should occur after all index reordering is complete.
- Quantization (step 5) compresses data only after the final vertex order is established.
- Index filtering (step 6) cleans up degenerate geometry introduced by previous transformations.
- Shadow indexing (step 7) is optional and creates specialized buffers for depth-only passes.
Frequently Asked Questions
Can I skip steps in the mesh optimizer pipeline?
You can skip optional steps like overdraw optimization and shadow indexing, but you should not skip mandatory stages like indexing or reorder core steps. Vertex-cache optimization must precede vertex-fetch optimization, and quantization must follow all reordering. Skipping indexing will cause subsequent functions to fail or produce incorrect results because they expect compact, unique vertex buffers.
Why must vertex quantization happen after vertex-fetch optimization?
Quantization compresses vertex attributes into smaller formats (such as half-precision floats). If you quantize before reordering vertices for fetch efficiency, you waste computational effort compressing data that will be rearranged anyway. Additionally, vertex-fetch optimization determines the final memory layout, so quantization should only be applied to the final vertex order to achieve maximum cache locality for the compressed data.
What happens if I run overdraw optimization before vertex-cache optimization?
The overdraw optimizer in src/overdrawoptimizer.cpp rearranges triangles to minimize pixel shader work, but this reordering typically destroys the vertex-cache friendly ordering established by meshopt_optimizeVertexCache. The pipeline places overdraw optimization after cache optimization because it is less sensitive to small changes in vertex reuse, whereas the cache optimizer creates a specific access pattern that overdraw optimization respects when given the threshold parameter (typically 1.05).
Is shadow indexing always necessary?
No, shadow indexing is optional and only required when you render shadow maps or depth-only passes. The standard pipeline produces an index buffer optimized for full forward rendering. If your application uses the same mesh for both color and depth passes, generating a separate shadow index buffer with meshopt_generateShadowIndices can improve depth-pass performance by optimizing for position-only vertex fetches, but it consumes additional memory for the extra index buffer.
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 →