Benchmark
Which 3D file formats record which way is up, and which leave it to be guessed
Why does a model arrive rotated, lying on its side, after a format conversion?
Result
None of the five formats writes an up axis into the file. Not one. What decides the answer is the specification, and the two specifications that decide it point in different directions: glTF fixes +Y as up, 3MF puts +Z toward the top of the build platform, and STL, OBJ and PLY say nothing at all. The geometry itself crosses intact every time, so a part authored 30 mm tall arrives with its 30 mm still in the file. It is the reading that differs: a consumer that applies the Y-up convention turns that 30 mm height into 20 mm and lays the part on its side, a 0.67x error with nothing in the bytes to contradict either interpretation.
Does the file say which way is up?
| Format | Up axis stated in the bytes | Up axis fixed by the spec | What the spec says | Agrees with Z up authoring |
|---|---|---|---|---|
| GLB | No | +Y | glTF defines +Y as up | No |
| STL | No | none | the specification is silent | no rule to agree with |
| OBJ | No | none | the specification is silent | no rule to agree with |
| PLY | No | none | the specification is silent | no rule to agree with |
| 3MF | No | +Z | the z-axis increasing to the top of the output field | Yes |
The first column is the measurement and the second is documentation. No file in the test carries the fact; two of the formats have it decided for them by a specification, and those two specifications disagree with each other. The other three leave it entirely to whichever program opens the file.
What the geometry does, and what a wrong reading costs
| Format | Extents after a round trip (mm) | Numbers survived | Height read as authored (mm) | Height after a Y-up conversion (mm) | Error factor |
|---|---|---|---|---|---|
| GLB | 10, 20, 30 | Yes | 30.0 | 20.0 | 0.6667 |
| STL | 10, 20, 30 | Yes | 30.0 | 20.0 | 0.6667 |
| OBJ | 10, 20, 30 | Yes | 30.0 | 20.0 | 0.6667 |
| PLY | 10, 20, 30 | Yes | 30.0 | 20.0 | 0.6667 |
| 3MF | 10, 20, 30 | Yes | 30.0 | 20.0 | 0.6667 |
Read the third and fourth columns together. Every format returns exactly the numbers it was given, so nothing is damaged or dropped in transit. The 30 mm becoming 20 mm in the next column is not a loss of data, it is the same data read against a different axis.
3MF writes down the unit and does not write down the orientation
The most telling line in the raw output is the 3MF one. Its model element carries these attributes: unit, and six XML namespace declarations. Nothing else. That is the same element which, in our unit declarations benchmark, was the only place in any of these five formats where a unit of length was actually recorded. So 3MF demonstrably had the opportunity and the mechanism to record orientation in the same breath, and did not. The orientation lives in the specification prose instead, which means a reader that has not read the prose has nothing to go on.
This is not a limitation of 3D formats, only of these ones
Other formats do record it. FBX carries UpAxis in its GlobalSettings block, USD carries upAxis as stage metadata, and STEP attaches an explicit axis placement to each shape. None of those three could be written by the library used here, so they are cited from their documentation rather than measured, and they are listed under what this does not show. They matter anyway, because they establish that the fact is perfectly recordable. The five formats a model actually travels through on the way from a generator to a printer are the ones that do not record it.
Why the failure looks like a rotation rather than an error
Nothing is invalid at any point, which is why no program reports a problem. The file is well formed, the numbers are correct, and each side of the exchange applies a defensible convention. The part simply arrives lying on its side, and the height a slicer reports is the depth the designer intended. When the two conventions differ by 90 degrees about X, as Y up and Z up do, a 10 x 20 x 30 box is read as 10 x 30 x 20. Anything that then gets decided from the height, build orientation, support generation, whether the part fits the machine, is decided from the wrong number.
How this was measured
One box measuring 10 x 20 x 30 mm with its minimum corner at the origin, authored Z up so that the 30 mm dimension is the height, which is what CAD, 3D printing and every slicer assume. Three distinct extents mean any axis permutation shows up in the result rather than having to be inferred, and the corner at the origin would expose a mirror. Each file was written, read back and measured, and separately opened as a raw container: the GLB JSON chunk was parsed out of the binary header, the STL 80 byte header was read, OBJ and PLY header lines were read as text, and the 3MF archive was opened as a ZIP with every attribute on its model element listed.
- Software and versions
- trimesh 5.1.0, numpy 2.5.2
- Date measured
- 2026-09-11
- Script
scripts/measure-up-axis.py- Raw data
- up-axis-2026-09-11.json
This data is published under CC BY 4.0. Reuse the numbers freely, with a link back so a reader can check the method.
What this does not show
Stated so the result is not read wider than it goes.
- FBX and USD, both of which DO record an up axis in the file: FBX in GlobalSettings.UpAxis and USD in the upAxis stage metadatum. trimesh cannot write either, so these are stated from their documentation rather than measured, and they are the counter-example that matters: the fact is recordable, the five formats measured here just do not record it.
- STEP, which carries an explicit axis placement per shape, for the same reason.
- What any named application does on import. Behaviour differs between Blender, Cura, PrusaSlicer, Unity and Unreal, and each states its own convention in its own documentation.