How to convert GLB to STL
- Load the GLB containerSelect a .glb file. The parser validates the magic 0x46546C67 and version 2 header, then reads the JSON and BIN chunks, trimming four-byte padding before interpreting accessors and bufferViews.
- Decode meshes and primitivesEach mesh primitive contributes POSITION, NORMAL and index data. Primitives with non-triangle modes are expanded into plain triangle lists; Draco- or meshopt-compressed primitives need the workspace, which loads the decoders.
- Bake node transformsThe scene node tree is walked once. Parent matrices multiply down to each mesh, so vertex positions land in world space and the hierarchy itself disappears from the output.
- Merge into one flat meshAll primitives are concatenated into a single geometry holding only position and normal attributes, with node transforms already baked into the coordinates. The merge appends vertices and offsets the indices; it does not compare them, so a corner shared between two primitives is written twice and seams stay exactly as the author left them.
- Write binary STLThe exporter emits an 80-byte header, a uint32 facet count and 50 bytes per facet. The result is a single .stl blob that any slicer opens.
What happens when GLB is converted to STL
What a GLB file actually contains
GLB is the binary form of glTF 2.0. After a twelve-byte header it carries a JSON chunk that declares scenes, nodes, meshes, accessors and bufferViews, and a BIN chunk that holds the raw typed arrays those accessors point into. Textures may be embedded as images in the BIN chunk, and the JSON may reference extensions such as Draco compression or KTX2 textures.
What survives the conversion
Only triangles survive. Vertex positions are kept, normals are kept or recomputed when missing, and node transforms are baked into the coordinates. Everything else the container can express has no destination in STL: PBR materials, base colour factors, texture images, texture coordinates, skins, animation channels, morph targets, cameras and lights are all dropped.
Why indexed meshes grow instead of shrinking
glTF primitives are usually indexed: a position array plus a buffer of indices, so a corner shared by four faces is stored once. Binary STL has no equivalent mechanism. Each of its 50-byte records carries three complete vertices, and there is no index table in the format for a reader to consult. Conversion therefore expands the index buffer into explicit per-facet vertices, which multiplies the stored position data by roughly the corner-reuse factor. No pass deduplicates the result and no welding hash runs anywhere in the exporter, so the facet count of the output equals the triangle count of the source scene. That is the number to watch: a modest-looking GLB of highly shared geometry can still produce an STL several times heavier in bytes than the mesh data inside the original file.
Why the result has no scale anyone can agree on
glTF documents are written in metres by convention, but STL stores plain numbers with no unit at all. The converter does not rescale coordinates, so the numeric values pass through untouched. A slicer will treat the STL in whatever unit it is configured for, so check the bounding box before printing.
The smooth normals in your GLB are recomputed, not copied
glTF stores a normal per vertex, which is why a low-poly sphere can shade as if it were round. STL stores a normal per facet, and the exporter does not read the attribute at all: for every triangle it takes the three corner positions, subtracts to get two edge vectors, crosses them and writes the unit result into the record. So the shading information is discarded twice over, once because the format cannot hold a different normal per corner, and once because the code never looks at the source normals. The geometry is untouched, but a viewer that trusts the stored normals shows the mesh as faceted, and a slicer that uses them to decide inside from outside sees a hard direction change at every seam that used to be smooth. This is why a converted GLB often looks rougher than it did in the browser.
The up axis is carried across unchanged
glTF is defined as right-handed with +Y up, and the loader keeps the scene exactly that way. STL has no field for an up axis at all; the three floats in a facet record are simply x, y, z, and the convention among slicers and CAD tools is that +Z is up. Nothing in this pipeline rotates the model to compensate. A GLB authored for a game engine therefore arrives lying on its back, and the fix belongs in the slicer: rotate ninety degrees about X, or use the import transform, before scaling to fit the plate. The same absence of metadata is what makes the next problem harder to see, because an import dialog that shows a plausible orientation says nothing about whether the numbers are metres or millimetres.
Animations, skins and morph targets have nowhere to go
A GLB is often more than a mesh: the JSON chunk may declare animation channels that sample node transforms over time, a joint hierarchy driven by a skin, and morph targets with weights. A facet record has three vertices, a normal and two unused bytes, so there is no slot for a time, a joint index or a weight. The loader hands the scene to the converter and the animation list is dropped with it; the triangles that survive come from the base geometry, which for a rigged character is the bind pose. The output is therefore one static mesh in whatever pose the file stores by default. If the export needs a specific frame, pose it and bake it in the modelling tool first, then convert the baked result.
GLB vs STL
| GLB source | STL output | |
|---|---|---|
| Feature | GLB source | STL output |
| Container | Binary glTF 2.0: JSON chunk plus BIN chunk | 80-byte header plus flat facet records |
| Scene structure | scenes, nodes, meshes, primitives | One merged triangle soup |
| Transforms | Node matrices and TRS values | Baked into vertex coordinates |
| Materials | pbrMetallicRoughness and textures | None |
| Texture coordinates | TEXCOORD_0 accessors | Not representable |
| Animation | Skins and animation channels | Not representable |
| Compression | Optional Draco or meshopt extensions | Uncompressed binary |
| Units | Metres by convention, undeclared | No unit |
| Output files | One self-contained binary | One .stl file |
Supported formats
| Format | Read | Write | Notes |
|---|---|---|---|
| STEP (.step .stp) | Yes | – | ISO 10303 B-rep, AP203 / AP214 / AP242 |
| IGES (.iges .igs) | Yes | – | Entity-based surfaces, up to 5.3 |
| BREP (.brep) | Yes | – | OpenCascade native B-rep |
| STL (.stl) | Yes | Yes | Binary and ASCII triangles, no units |
| OBJ (.obj) | Yes | Yes | v / vn / f records; .mtl references ignored |
| PLY (.ply) | Yes | Yes | Binary position and normal arrays |
| GLB / glTF (.glb .gltf) | Yes | Yes | glTF 2.0, binary or JSON with data URI |
| 3MF (.3mf) | Yes | Yes | OPC/ZIP package, declares millimetres |
| FBX (.fbx) | Yes | – | Binary and ASCII, static mesh only |
| DAE (.dae) | Yes | Yes | Collada scene graph is flattened |
| 3DS (.3ds) | Yes | – | Legacy chunk-based 3D Studio format |
| AMF (.amf) | Yes | – | XML, ZIP-compressed packages supported |
| USDZ (.usdz) | Yes | Yes | ZIP containing a USD stage |
Known limitations and why a conversion can fail
Draco and meshopt compression is decoded on the fly
A GLB whose primitives declare KHR_draco_mesh_compression or EXT_meshopt_compression stores its vertices compressed, so no triangle exists until the matching decoder has run. The workspace wires both decoders and loads them lazily from files served by this site, which means a compressed primitive is decoded first and then flattened like any other, and a Draco cube with a thousand triangles converts and exports normally. Compression changes how the container stores its vertices, not the facet list an STL can hold, so the downloaded file is the same one an uncompressed GLB of the same mesh produces.
Very large scenes expand, and memory is the ceiling
Because expansion is unavoidable, the triangle count drives everything downstream. Positions and normals are held as 32-bit float arrays in the tab, the scene graph keeps a geometry per primitive until the merge finishes, and the serialised STL is then built as one contiguous block, so peak use sits well above the size of the file you eventually download. WebAssembly linear memory cannot grow past roughly four gigabytes in a 32-bit module, and a browser tab rarely gets that much in practice. Very dense scans therefore fail with a memory or script error rather than producing a truncated file. Splitting the scene, or converting a decimated copy, is the practical remedy.
Colour never reaches STL
STL has no material system beyond an optional attribute word that slicers ignore. A coloured or textured GLB converts to a colourless mesh; paint it in your slicer or slicer-side tools instead.
Frequently asked questions
Can I convert on this page itself?
No. This page is the reference for the GLB to STL path, and the conversion itself runs in the workspace that the button above opens, with both formats already selected. Your file is read in that browser tab and the download is written locally, so there is no upload stage in either place. Keeping the documentation and the engine on separate addresses lets file support and detail levels change behind one workspace without breaking the page you are reading.
Will my textures survive?
No. STL stores triangles only. Base colour textures, PBR parameters and texture coordinates have no place in the format, so the converter drops them rather than pretending to keep them.
Why is my model the wrong size in the slicer?
glTF is metres by convention; STL has no unit. The numbers are not rescaled during conversion, so import the STL at the unit your slicer expects and verify the bounding box before printing.
Can I convert a whole animated scene?
The static geometry converts; animation does not. Skins, joints and animation channels are skipped, and each mesh is frozen at its current node transform.
Does the converter merge duplicate vertices?
No, and binary STL is the reason. A glTF primitive can be indexed, storing each shared corner once and referring to it from an index buffer, while a binary STL facet carries its own three complete vertices and the format has no index table at all. So the exporter walks the indices and writes one 50-byte record per triangle, which inflates the vertex data by roughly the corner-reuse factor instead of compacting it. Nothing is deleted and no coordinate is moved, which is why the triangle count reported after loading predicts the size of the download.
Do Draco or meshopt compressed GLB files convert?
Yes. Both decoders are wired into the workspace and loaded lazily from files served by this site, so a primitive declaring KHR_draco_mesh_compression or EXT_meshopt_compression is decoded before any triangle is written. The flattened facet list that reaches the STL is the same one an uncompressed GLB of the same mesh produces, because a facet record has no field for a quantised position either way.
What happens to a GLB with several meshes and materials?
Every drawable mesh is transformed into world space and appended into one flat triangle list. Materials are the first casualty: glTF associates primitives with metallic-roughness or specular-glossiness descriptions, and a facet record has no field for any of it. Skinned rigs, morph target weights, node names, cameras and lights are dropped with them. If the model needs to keep its appearance, converting to OBJ or GLB preserves geometry with material references, whereas STL cannot represent them even in principle.
Is the exported STL watertight?
Nothing in the pipeline certifies it. A glTF mesh is a rendering asset, and authors routinely build a cube from six independent quads with duplicated corners, or leave hairline gaps between parts that a game engine would never notice. Because the format stores no topology, a slicer has to weld near-identical vertices itself before it can decide whether the shell closes. A model that looks seamless in a viewer can therefore arrive in a slicer as an open or self-intersecting volume, and that is a property of the source mesh rather than of the conversion.
Why does my model look faceted after conversion?
Because smoothing information cannot be stored. glTF gives each vertex its own normal, so a coarse sphere shades as if it were round, while binary STL gives each facet one normal computed from the three corner positions. The exporter performs that cross product itself and never reads the source normals, so every shared smooth direction is replaced by a flat per-triangle one. The vertices stay exactly where they were and the silhouette is unchanged, which is why the mesh still measures the same size; only the shading is harsher. A slicer ignores normals entirely, so this is a viewing artefact rather than a loss of geometry.
References
- Khronos glTF 2.0 specification
- three.js documentation — loaders and exporters
- How this site is built and tested
Related converters
All converters
Last updated: