CLI tool
zeux/meshoptimizer avatar
zeux/meshoptimizer

meshoptimizer: a C/C++ mesh pipeline library, plus gltfpack

Mesh optimization library that makes meshes smaller and faster to render

8,474 stars751 forksC++MIT

At a glance

What is it?
meshoptimizer is an MIT-licensed C/C++ library of mesh optimization algorithms, shipped alongside the gltfpack command-line tool and a single-header cluster LOD library. It is aimed at engine and asset-pipeline developers who control their own vertex and index data.
Who is it for?
Adopt meshoptimizer if you have a C/C++ engine or asset pipeline that already owns its vertex and index buffers, or if you just want gltfpack to shrink glTF files without writing code. Do not adopt it expecting a Blender-style editor, a Rust-native API, or a Unity package: the Rust path goes through the separate meshopt crate, and the README documents no Unity or Blender integration.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem meshoptimizer solves, and who it is written for

A GPU does not render the mesh you authored. It renders the vertex and index buffers you hand it, and the cost of the vertex shader, the post-transform cache, overdraw and vertex fetch all depend on how those buffers are laid out. meshoptimizer exists to reshape that data before it reaches the driver. The README frames the purpose directly: the library provides algorithms that optimize meshes for the GPU pipeline stages, plus algorithms that reduce mesh complexity and storage overhead.

The audience is narrow and specific. The library exposes a C and C++ interface for every algorithm, and the README notes that other languages reach it through FFI, with P/Invoke as the named example. If you want Rust, the README points at the separate meshopt crate rather than a binding inside this repository. JavaScript coverage is partial: some algorithms are available through meshoptimizer.js. That split matters when you are choosing where to put the optimization step. A C++ engine can link the library and run the pipeline at import time. A web tool has to accept the subset the JS package exposes, or shell out to gltfpack.

Two companion projects ship alongside the library. gltfpack is a command-line tool that optimizes glTF files automatically, and clusterlod.h is a single-header C/C++ library for continuous level of detail using clustered simplification. Both live in the same repository, which means the practical entry point for many teams is not the C API at all but running gltfpack over an asset folder.

The seven-step pipeline and why the order is fixed

The README is explicit that the order matters, and it lists seven stages: indexing, vertex cache optimization, optional overdraw optimization, vertex fetch optimization, vertex quantization, index filtering, and optional shadow indexing. Each stage assumes the previous one has already run. Indexing produces a remap table and a rebuilt vertex buffer; the later stages then operate in place on those buffers.

The first stage is where most of the conceptual weight sits. meshoptimizer assumes a vertex buffer and an index buffer, and the README states that for the algorithms to work well, and for the GPU to render efficiently, the vertex buffer must contain no redundant vertices. The remap is generated from binary equivalence of the input vertices, which the README describes as generally a reasonable default. Binary equivalence compares all input bytes, including padding, so a vertex struct with gaps must be zero-initialized or two vertices that look identical will stay separate.

That default has a known failure mode, and the README names it: floating point drift in some attributes can cause extra vertices to be generated. The prescribed fixes are to quantize the affected attributes first, most importantly normals and tangents, or to switch to meshopt_generateVertexRemapCustom and supply a comparison function with a tolerance. The README's example compares two tangent components with fabsf(lv.tx - rv.tx) < 1e-3f. That is a real design trade-off rather than a bug: exact byte comparison is fast and predictable, and the escape hatch costs you a custom predicate.

One convention is worth internalizing early. The library generally works with 32-bit unsigned int indices, but the C++ template overloads accept any integer type for index data. Remap tables, by convention, are always unsigned int.

Installing meshoptimizer and running the first remap

The README gives two acquisition paths. You can clone the tagged release, or download the zip archive from GitHub. Distribution packages exist for ArchLinux, Debian, FreeBSD, Nix and Ubuntu, plus a Vcpkg port and a Conan package. The clone command in the README pins the tag:

bash
git clone -b v1.2 https://github.com/zeux/meshoptimizer.git

For building, the README describes the library as a header plus a set of C++ source files, and offers two options: use CMake, either standalone or as part of your project, or add the source files to your own build system. The repository layout supports the second option. The Makefile builds a static library from every file matching src/*.cpp, and the README states the sources are organized so you only need to add the files for the algorithms you use. Amalgamated builds are also supported by concatenating the source files into one .cpp.

Once the header is on your include path, the first real operation is generating a remap table. This snippet is the README's unindexed-mesh case, where face_count * 3 vertices arrive with no index buffer:

c++
size_t index_count = face_count * 3;
size_t unindexed_vertex_count = face_count * 3;
std::vector<unsigned int> remap(unindexed_vertex_count); // temporary remap table
size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, index_count,
    &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex));

The NULL in the second argument is the index buffer, and it is NULL precisely because the input is unindexed. If your mesh already has an index buffer, the README says to pass that buffer instead, along with the correct source vertex count. The return value, vertex_count, is the size of the compacted vertex buffer you now allocate. The README then has you remap both buffers in place:

c++
meshopt_remapIndexBuffer(indices, NULL, index_count, &remap[0]);
meshopt_remapVertexBuffer(vertices, &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex), &remap[0]);

After this, the resulting buffers are the input to the remaining pipeline stages. The README does not document an undo for any of these operations, so keep the original buffers until you have compared the rendered result.

gltfpack, and the npm build that is not equivalent

gltfpack is the part of the project you can adopt without writing C++. It is a command-line tool that optimizes glTF files automatically, and the README lists two ways to get it: a pre-built binary on the Releases page, or the npm package. The README states a preference rather than leaving it neutral: native binaries are recommended, because they are more efficient and support texture compression.

That sentence is the most consequential line in the distribution section. If you install gltfpack through npm and your assets carry textures you wanted compressed, you have chosen the build that does not do it. The repository also shows a third path for people who build from source. The Makefile has a BASISU variable that, when defined, adds -DWITH_BASISU to the gltfpack compilation flags and links libbasisu_encoder.a from the BASISU path. So texture compression in a source build is gated on you supplying a Basis Universal tree yourself.

There is also a WebAssembly build path in the same Makefile, targeting wasm32-wasi through a WASI SDK, with a fixed 36864-byte stack and 65536 bytes of initial memory, and an explicit export list of decoder functions such as meshopt_decodeVertexBuffer, meshopt_decodeIndexBuffer and the filter decoders. That is a decoder, not the full optimizer. It tells you where the project sees WASM fitting: decompression on the client, optimization on the server or at build time.

Where meshoptimizer is the wrong tool

The library is not an editor and does not pretend to be. It has no scene graph, no material system, and no import UI. It operates on vertex and index buffers you already extracted, which means the work of getting from a DCC file to those buffers is yours. gltfpack narrows that gap for glTF specifically, but the README describes it as a glTF tool, and nothing in the README suggests it reads other formats.

The binary-equivalence default is the second place teams get surprised. If your exporter writes normals with small float differences across shared vertices, meshopt_generateVertexRemap will treat them as distinct and your vertex count will not drop as far as you expected. The README's own remedy, quantizing normals and tangents before the remap or writing a custom comparator, adds a step that has to be validated visually. A tolerance that is too loose merges vertices that should stay apart, and the README gives no guidance on choosing one beyond the 1e-3f example.

Language coverage is the third boundary. The README says the interface is C and C++, that other languages go through FFI, that Rust users should use the meshopt crate, and that JavaScript gets some algorithms through meshoptimizer.js. If your pipeline is Python, TypeScript or Go, you are either binding to the C interface yourself or using a wrapper the README does not name. There is no first-party package for those languages in this repository.

meshoptimizer against Draco, and against writing your own

Draco is the comparison people reach for, and the difference is in scope. Draco is a geometry compression library built around its own compressed format, so the artifact it produces is a Draco stream that a decoder reconstructs. meshoptimizer instead reshapes buffers that stay in ordinary vertex and index form, and adds codecs on top: index filtering and vertex quantization are pipeline stages, not a container format. If your runtime already consumes raw buffers and you want them smaller and friendlier to the GPU, that distinction is the whole argument. If you need a single compressed blob with a stable decoder on many platforms, Draco's model is closer to what you are describing.

The other alternative is doing nothing and writing the passes yourself. Vertex cache optimization is a reordering problem, and a naive implementation is not hard. What the library gives you is a set of algorithms that have been run against real meshes and a documented order in which to apply them. The README's insistence that the order is important is the useful part; getting the sequence wrong, for example quantizing before reindexing, wastes the work.

For level of detail specifically, the project ships clusterlod.h as a single-header C/C++ library for continuous LOD using clustered simplification. That is a different deliverable from the core library and worth reading separately if LOD generation is what you came for.

Maintenance, licensing and the cost of upgrading

The repository is not archived, and the last push was on 2026-09-21. Releases are tagged: v1.0 on 2025-12-08, v1.1 on 2026-04-02, and v1.2 on 2026-06-30. The README's install command pins v1.2, so the tag-based workflow is the intended one.

Upgrade cost depends on which surface you use. The core library is distributed as source, and the README says the source files are organized so you only add the ones for the algorithms you use. That means a version bump can be as small as replacing a handful of .cpp files and the header, with no build-system change. If you vendored the whole tree, or concatenated it into an amalgamated .cpp, you have fewer moving parts but a coarser diff.

Licensing is MIT, per the LICENSE.md file and the badge in the README. That is permissive and imposes no copyleft obligation on your own code. The one caveat visible in the repository is the optional Basis Universal encoder used by gltfpack when built with BASISU defined. That dependency is not part of meshoptimizer and carries its own terms, so if you ship a gltfpack build with texture compression enabled, check the Basis Universal licence separately. This is a description of what the files say, not legal advice.

Editorial conclusion

Adopt meshoptimizer if you have a C/C++ engine or asset pipeline that already owns its vertex and index buffers, or if you just want gltfpack to shrink glTF files without writing code. Do not adopt it expecting a Blender-style editor, a Rust-native API, or a Unity package: the Rust path goes through the separate meshopt crate, and the README documents no Unity or Blender integration. Before committing, verify three things against your own data: that binary vertex equivalence does not split vertices you consider identical (quantize normals and tangents first, or use meshopt_generateVertexRemapCustom), that the pipeline order you pick matches the seven steps the README lists, and which gltfpack binary you install, since the npm package lacks the texture compression the native builds support.

Frequently asked questions

How do I use meshoptimizer?

Include the header meshoptimizer.h and link the source files for the algorithms you need, then run your mesh through the pipeline in the order the README lists: indexing, vertex cache optimization, optional overdraw optimization, vertex fetch optimization, vertex quantization, index filtering, and optional shadow indexing. If you would rather not write C++, run gltfpack over your glTF files instead.

What is mesh simplification?

The README describes the library as providing algorithms to reduce mesh complexity and storage overhead, alongside algorithms that optimize meshes for GPU pipeline stages. The project also ships clusterlod.h, a single-header C/C++ library for continuous level of detail using clustered simplification.

How do I install meshoptimizer?

Clone the tagged release with git clone -b v1.2 https://github.com/zeux/meshoptimizer.git, or download the zip archive from GitHub. It is also packaged for ArchLinux, Debian, FreeBSD, Nix and Ubuntu, and available through a Vcpkg port and a Conan package.

Does meshoptimizer work with Rust or JavaScript?

The README says the library provides a C and C++ interface, and that Rust users should use the separate meshopt crate. A JavaScript interface for some algorithms is available through meshoptimizer.js.

Why does meshoptimizer leave more vertices than expected after reindexing?

meshopt_generateVertexRemap uses binary equivalence of vertex data, so floating point drift in attributes such as normals and tangents can produce extra vertices. The README suggests quantizing those attributes before generating the remap, or using meshopt_generateVertexRemapCustom with a tolerance-based comparison function.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. zeux/meshoptimizer on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/zeux-meshoptimizer.svg)](https://hysenlabs.com/projects/zeux-meshoptimizer)