STL, OBJ and glTF: the units and axes each one assumes

Why a part arrives a thousand times too big or on its side: what STL, OBJ, glTF and 3MF say about units and the up axis, and how schmelze writes each.

A 3D file is a list of triangles. The triangles are the easy part. The hard part is what the numbers mean: millimetres or metres, and which way is up. Each format answers that differently, and some do not answer at all.

Four formats, four answers#

FormatUnitUp axisWho decides
STLnot storednot storedcustom: slicers read millimetres, Z up
OBJnot storednot storedthe program that opens it
glTF 2.0metresYthe specification
3MFstored in the file, millimetres by defaultZthe specification

schmelze, the 3D tool on this site, writes the first three. Its scene is Y-up in millimetres, so each export turns the numbers into what the format expects: STL in millimetres with Z up, OBJ in millimetres with Y up, glTF in metres with Y up.

STL: binary, ASCII, and a trap in the header#

A binary STL is 80 bytes of free header text, a four-byte triangle count, then 50 bytes per triangle: a normal, three corners and two spare bytes. An ASCII STL says the same in words: solid, then facet normal, outer loop, three vertex lines, and so on.

The trap: many programs tell the two apart by checking whether the file starts with solid. But the 80-byte header of a binary file is free text, and some writers put solid there. A reader that trusts the first word then tries to read binary data as text and fails.

The reliable test is the size. A binary STL with n triangles is exactly 84 + 50 × n bytes, and n is written at byte 80. schmelze checks that first and only falls back to reading text when the size does not match. Its tests include a binary file whose header starts with solid, to keep it that way. And schmelze's own STL header never starts with solid, so other readers do not fall into the trap with its files.

The normals stored with each triangle are a hint at best. Most readers, schmelze included, work them out again from the order of the three corners, which is what really says which side is out.

Turning Z-up into Y-up without mirroring#

Changing the up axis sounds like swapping two coordinates: y becomes z. It is not. Swapping two axes mirrors the part, and a mirrored part looks right from most angles until you notice the thread turns the wrong way or the text on it reads backwards. The correct change is a rotation of 90 degrees: (x, y, z) becomes (x, −z, y). schmelze's tests check that its STL is right-handed, not mirrored, and that the stored normals agree with the winding.

The part that arrived a thousand times too big#

glTF says in its specification that one unit is one metre. For a while our glTF writer used millimetres anyway. A 20 mm part became 20 metres in any program that follows the specification.

Our tests did not notice. They wrote each file and read it back with our own reader, and the reader made the same assumption as the writer, so every round trip matched. All fifteen round-trip checks for glTF stayed green with the bug in place. An earlier axis bug went the same way: 39 of 40 tests passed with it.

So now schmelze's test suite hands all three formats to Blender, which applies each format's own conventions when it imports: glTF comes in from metres and Y-up, STL from Z-up, OBJ from Y-up. The same part has to land in the same place and at the same size by three different routes. The expected sizes come from the shapes' own parameters, never from the files.

OBJ: say what you mean#

OBJ has no field for units or the up axis. Every program assumes something, and they do not agree. schmelze writes millimetres and Y-up, the scene as it is, and states both in a comment at the top of the file so a person can at least read what was meant. Its OBJ reader accepts four-sided faces and negative indices, which some exporters write.

3MF: the format that fixed it#

3MF is a zip file with XML inside. It stores the unit, several objects and their colours and materials, and slicers add their own print settings to it. That makes it a better format to hand from one slicer to another. schmelze does not read 3MF yet. If you have one, your slicer can export the plate as STL.

What to send a slicer#

  • STL still works everywhere. Model in millimetres and it arrives the right size.
  • If a part comes in 25.4 times too small, it was modelled in inches. Scale by 25.4.
  • If it comes in 1000 times too small or too big, someone mixed metres and millimetres: usually glTF on one side.
  • If it lies on its side, one program used Y-up and the other Z-up.

You can open an STL in your browser with schmelze, read its size in millimetres and save it as OBJ or glTF with the right unit for each.