# tinyobjloader: a single-header Wavefront OBJ loader for C++ and C11

> tinyobjloader parses .obj and .mtl files through one C++11 header or a separate C11 implementation, and it now ships its own tessellation library. Here is how the two loaders differ, how to get a mesh out of one, and where the design stops being enough.

**tinyobjloader/tinyobjloader** — Tiny but powerful single file wavefront obj loader

- Repository: https://github.com/tinyobjloader/tinyobjloader
- Stars: 3,893 · Forks: 650
- Language: C++
- License: NOASSERTION
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/tinyobjloader-tinyobjloader

## What tinyobjloader is for, and who ends up using it

Wavefront OBJ is a text format, and writing a parser for it is a week of work that produces nothing a user can see. tinyobjloader exists to remove that week. The README describes it as a "Tiny but powerful Wavefront .obj/.mtl loader" and says it is good for embedding an .obj loader into a renderer. That is the honest framing: this is a component, not an application.

The audience is narrow and specific. You are writing a global illumination renderer, a mesh converter, a voxelizer, a viewer, or a build step that ingests geometry, and you are doing it in C++11 or C11. The repository ships examples that match those jobs: examples/viewer for an OpenGL viewer, examples/voxelize for a voxelizer, examples/callback_api for a custom loading path, plus examples/obj_sticher and examples/skin_weight. If your pipeline is Python, the python folder and the PyPI package named tinyobjloader cover that, but the bindings are a wrapper around the C++ loader, not a separate design.

The project has been around long enough that its release history is a warning as much as a credential. The most recent tagged release listed is v2.0-rc1 from 2019-06-19, and the README still calls the release branch a release candidate. Development has not stopped: the last push was on 2026-06-19. What that means in practice is that the branch moves while the tags do not, so pinning to a tag gives you a 2019 snapshot and tracking release gives you whatever landed most recently.

## The two loaders: tiny_obj_loader.h versus tiny_obj_c.c

The most important thing to understand about this repository is that it contains two implementations that do not share code. The README calls them "two co-equal, independently usable implementations", and the layout confirms it: tiny_obj_loader.h on one side, tiny_obj_c.c, tiny_obj_c.h, tiny_obj_c_impl.inc and tiny_obj_c_pub.inc on the other.

The C++11 path is the single-header loader. You copy tiny_obj_loader.h into your project, define TINYOBJLOADER_IMPLEMENTATION exactly once, and compile. The README states there is no dependency except the C++ STL. It supports groups, vertex colors as an extension, texcoords, normals, crease tags (which the README notes are OpenSubdiv specific and not in the Wavefront specification), smoothing groups, double precision, and a callback API. Its primitive checklist marks faces and lines as done and points, curves, 2D curves, surfaces and free-form curve or surface as unchecked.

The C11 path is new and deliberately different. According to the release notes entry for 19 Jun, 2026, it is a from-scratch pure C11 loader with its own polygon tessellation library, tobj_tess. It is dual float and double precision, freestanding-capable with no libc dependency in the core, and it parses and retains points and free-form geometry. It is not header-only, which is the trade: you get a smaller runtime surface and C compatibility, and you give up the copy-one-file install.

If you are choosing between them, the primitive support table is the deciding fact. The C++ loader does not claim points or free-form geometry. The C11 loader does.

## How the parsed data is laid out

tinyobjloader does not hand you a list of triangles with positions baked in. It separates vertex attributes from topology, and the README documents that split with ASCII diagrams.

attrib_t holds flat arrays: vertices at 3 floats per vertex, normals at 3 floats per vertex, texcoords at 2 floats per vertex, and colors at 3 floats per vertex when present. A shape_t::mesh_t holds no vertex data at all. It holds indices into attrib_t, a num_face_vertices array giving the vertex count per face (3 for a triangle, 4 for a quad, 5 or more for an N-gon), and an index array. The README's diagram shows how num_face_vertices slices the index array into faces.

This is an indexed, face-preserving format. The loader does not silently triangulate unless you ask: the README notes that when the triangulate flag is true, behavior changes, and the sentence is cut off in the documentation. That matters because it means the default output can contain quads and N-gons, and any downstream code that assumes three indices per face will read the wrong data. Check the flag before you index into the arrays.

The vertex color support is an extension, not part of the Wavefront specification, and the README links to a Blender Stack Exchange question about getting vertex-painted OBJ files into Blender. Treat colors as optional and test with a file that has them and one that does not. Unknown material attributes are returned as a key-value map with string values, so a .mtl file with vendor-specific fields will not fail the parse; you will find them in that map.

## Installing it and loading your first mesh

There is no package manager step for the C++ header. The README gives one instruction: copy the header file into your project and make sure TINYOBJLOADER_IMPLEMENTATION is defined exactly once. That means one translation unit defines it before including the header, and every other file includes the header without it.

The repository's Makefile builds the example loader with double precision enabled, which is a useful default to copy because it exercises the HPC path the README mentions. Its first rule compiles loader_example.cc with -DTINYOBJLOADER_USE_DOUBLE=1 and -std=c++11, so running make in the repository root produces a loader_example binary.

```bash
make
```

The Makefile also notes that fast_float is bundled by default and that TINYOBJLOADER_DISABLE_FAST_FLOAT opts out of the bundled parser, which is the switch to reach for if the bundled number parsing conflicts with yours. Its commented-out strict compilation lines show TINYOBJLOADER_ENABLE_THREADED=1, the define that turns on the threaded path.

For the Python binding, the package is on PyPI and the build backend is setuptools with pybind11. The pyproject.toml requires pybind11>=2.10.0 and pins setuptools_scm below version 8, with a comment explaining that setuptools_scm>=8 is unsupported in the py3.6 cibuildwheel environment. The cibuildwheel configuration skips cp38 and cp314t and skips aarch64 builds because the build notes call the docker plus qemu path too slow.

```bash
pip install tinyobjloader
```

The README notes the precompiled binary on PyPI is manylinux1-x86_64 only, so on other platforms expect a source build and the pybind11 toolchain that comes with it.

## The optimized loader and what it costs you

The release notes entry for 22 May, 2026 adds LoadObjOpt, described as an optimized in-header loader with optional multithreading and SIMD. The Makefile shows the related switch: TINYOBJLOADER_ENABLE_THREADED=1 appears in the commented-out strict compilation lines.

This is the part of the project where the documentation is thinnest. The README announces the feature and points at a section for details, but the text available does not describe the threading model, whether the SIMD path is selected at compile time or runtime, or what the memory trade is. Multithreaded parsing usually means either a parallel pass over the file or a parallel build of the attribute arrays, and those have very different memory profiles. Before enabling TINYOBJLOADER_ENABLE_THREADED you should read the header itself, because that is where the answer is.

The same caution applies to the C11 loader's optional multithreading, SIMD and mmap support. The README lists them as optional and does not describe when mmap is appropriate. On a network filesystem or a file that another process is rewriting, mmap changes the failure mode from a clean read error to a signal. If the C11 loader is the reason you are here, budget time for reading tiny_obj_c.h rather than the README.

## Triangulation is the sharp edge

The README is unusually direct about a defect. The 29 Jul, 2021 entry says Mapbox's earcut was added for robust triangulation, and then adds that there is still an issue in the built-in triangulation algorithm, linking to issue 319. That sentence has been in the README since 2021 and the issue is described as open in the text.

So the practical question is which triangulator you are getting. The repository contains a mapbox directory and deps, and the third-party license section lists mapbox earcut.hpp under the ISC License. If your build picks up earcut, you get the robust path. If it falls back to the built-in algorithm, you get the path with the known bug. The README does not say how the choice is made at compile time, and this is the single most important thing to confirm before shipping geometry that depends on triangulation.

The other limitation is scope. OBJ is a geometry and material format with no scene graph, no animation, no skinning weights in the base specification, and no compression. If your content pipeline has moved to glTF, this loader is the wrong tool, and the repository's own example list shows the gap: examples/skin_weight exists as an example, not as a documented feature in the main feature list. The TODO section is also candid, listing a broken obj_sticker example and a need for more unit tests.

## Alternatives and the difference in approach

The comparison people reach for is tinyobjloader versus assimp. The difference is architectural, not a matter of speed. Assimp is a multi-format import library with a post-processing pipeline: it reads many formats into one internal scene representation and then runs steps like triangulation, vertex cache optimization and normal generation over that scene. tinyobjloader reads one format and stops. It gives you the attribute arrays and the face index structure and leaves triangulation, welding and optimization to you.

That makes assimp the better choice when you need to accept whatever format a client sends, and tinyobjloader the better choice when OBJ is the only format you will ever see and you want to control every step after the parse. The cost of the tinyobjloader approach is that the responsibility for correctness moves to your code: if you skip triangulation and your renderer assumes triangles, that is your bug, not the library's.

Fast_obj is the other name that comes up, and it sits at the opposite end of the same axis. It is a fast OBJ parser with a minimal API. Where tinyobjloader offers a callback API, an optimized in-header path, double precision and a separate C11 implementation, a minimal parser offers one code path and less to read. If you do not need vertex colors, crease tags, the callback API or double precision, the smaller surface is a real advantage, because there is less behavior to verify.

## Maintenance, licensing and upgrade cost

The repository is not archived, and the last push was on 2026-06-19. That is a recent change to the release branch, and the recent additions (the C11 loader in June 2026, LoadObjOpt in May 2026) show the project is still being worked on. That does not make the release tags current: the newest listed tag is v2.0-rc1 from 2019-06-19, and the README still calls the branch a release candidate. If your policy is to depend on tagged releases, you are depending on a 2019 snapshot while the branch has moved for seven years.

Licensing is MIT for the project itself, and the README states that plainly. The third-party components carry their own terms: pybind11 is BSD-style and mapbox earcut.hpp is ISC. The pyproject.toml declares the package license as "MIT AND ISC", which is the accurate expression for the distributed bundle rather than MIT alone. If you vendor only tiny_obj_loader.h, the ISC component does not travel with it; if you build the Python wheel, it does. That is a packaging question, not a legal one, and your own counsel is the right place to settle it.

The upgrade cost is mostly the branch-versus-tag gap. Because the C++ and C11 loaders are independent, a change to one does not force a change to the other, which limits blast radius. The risk sits in the optimized paths, where the documentation is thin enough that reading the header is the only way to know what a define turns on.

## Conclusion

Adopt tinyobjloader if you are writing a renderer, a converter or a tool in C++11 or C11 and want OBJ parsing without a dependency tree, and pick the C11 loader if you need points or free-form geometry, since the C++ path lists those as unchecked. Do not adopt it if your assets arrive as glTF, FBX or USD, or if you need a documented rollback and upgrade path, because the README describes no versioning policy beyond recommending the release branch. Verify first that your OBJ files triangulate cleanly: the README states that issue 319 in the built-in triangulation algorithm is still open, so run your own models through the loader and inspect the triangle count before you commit.

## FAQ

### How do I install tinyobjloader for C++?

There is no install step for the C++ loader. The README says to copy tiny_obj_loader.h into your project and make sure TINYOBJLOADER_IMPLEMENTATION is defined exactly once, in one translation unit.

### How do I use tinyobjloader to load an OBJ file?

Include the header, define TINYOBJLOADER_IMPLEMENTATION in one file, and call the loader; the parsed data lands in attrib_t as flat vertex arrays plus a shape_t::mesh_t holding indices and a num_face_vertices array. The repository's loader_example.cc is the reference for the call sequence, and the README points to it for details.

### What is a .OBJ file in C++?

In this context it is Wavefront OBJ, a text geometry format with a companion .mtl material file, and tinyobjloader is a parser that turns it into indexed vertex, normal, texcoord and color arrays. The README describes the loader as suitable for embedding into a renderer.

## Sources

- [Issues](https://github.com/tinyobjloader/tinyobjloader/issues)
- [README](https://github.com/tinyobjloader/tinyobjloader/blob/release/README.md)
- [Releases](https://github.com/tinyobjloader/tinyobjloader/releases)
- [tinyobjloader/tinyobjloader on GitHub](https://github.com/tinyobjloader/tinyobjloader)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tinyobjloader-tinyobjloader
