Skip to content

glTF input

Sebastien DUMETZ edited this page Nov 3, 2026 · 1 revision

glTF and GLB input

objtools import also accepts .gltf and .glb. The model is rewritten rather than rebuilt: buffer views are copied whole, so interleaving, sharing and attribute encodings survive, and the rest of the JSON is kept, including nodes, skins, animations, morph targets, materials and supported extensions. See glTF extensions for which extensions those are.

What the output guarantees

The code states this contract in src/import/from_gltf/mod.rs, and the test suite checks it on the files it imports.

Layout

  • One .gltf, one external .bin and image files. Nothing is embedded, whatever the input was.

Geometry

  • Every primitive is an indexed triangle list, with no zero-area triangle.
  • Indices are u16, or u32 above 65,535 vertices, and all within their vertex count.
  • A primitive's attributes all have the same count, and no float attribute is non-finite.
  • POSITION bounds are computed from the data.
  • Float NORMALs used by a triangle have unit length. Quantized ones are left as they are.
  • Attributes keep the encoding, interleaving and sharing they had, and vertices stay in place.

Materials and images

  • A primitive has every UV set its material samples.
  • Every material, texture and image in the file is used.
  • An image is JPG or PNG, or the format of the extension selecting it, with a valid header.
  • A mimeType says what the image really is.

Extensions

  • Only supported extensions appear, and extensionsUsed lists exactly those in use.
  • extensionsRequired keeps what the input required, and adds KHR_texture_transform.

Hierarchy and georeferencing

  • Nodes keep their indices and the hierarchy is a strict tree. New parents are only appended.
  • extras.objtools records the origin and bounds in the source frame, adding to any earlier.

How OBJ's rules apply

Everything import checks or changes on an OBJ is applied to a glTF, adapted, or skipped because it only exists for how OBJ works.

Applied

OBJ behaviour For glTF input
.gltf with one external .bin; never overwrites; removes partial output on failure Same. GLB and embedded data are unpacked.
--images, --image-format, --image-quality Same, for every image rather than only base colour. Embedded images are always written as files.
Image signature and header checks Same, extended to WebP, AVIF and KTX2. A mimeType that contradicts the image's content is corrected with a warning.
Triangle lists only; other records omitted with a warning Strips and fans become lists. Points, lines and primitives without POSITION are omitted with counted warnings.
Zero-area faces skipped and counted Same, counted per primitive. Only the indices change, so vertices stay.
Smallest index type; restart values reserved Same. A non-indexed primitive gets sequential indices, and is an error if a 32-bit index can't address it.
Non-finite values and out-of-range indices are errors Same, reading through each accessor's encoding.
POSITION bounds computed from the data Same: recomputed, never trusted from the input.
A primitive without UVs gets an -untextured material copy Same, checking every texture slot including extension textures, and honouring KHR_texture_transform's texCoord, as in obj2gltf's splitIncompatibleMaterials.
KHR_texture_transform listed in extensionsRequired Same.
--no-color Drops COLOR_n.
Unused materials and empty meshes omitted; no triangles is an error Same, along with unused textures and images.
Origin, Bounds and float32 notes on stderr Same.

Adapted

OBJ behaviour For glTF input Why
All attributes written as float32 Attributes keep their source encoding and stay shared between primitives. Rewriting would enlarge the file, and could duplicate geometry.
Normals normalized; zero-length normals repaired and counted Same for float32 normals, but a zero-length one takes the normal of a face using it. Quantized normals are left as they are, and zero-length ones are counted in a warning. glTF has no shared position index to smooth over, and quantized values can't be exactly unit length.
--center subtracts the centre before float32 encoding See below. glTF has node transforms, and a quantized position can't always hold the shifted value. Digits lost when the source was encoded as float32 can't be recovered.
--convert-images converts anything other than JPG and PNG WebP, AVIF and KTX2 under their extension are kept by default, and --convert-images false accepts them. --convert-images true converts WebP, and is an error for AVIF and KTX2. The extension makes these formats valid glTF, and converting would lose quality.
extras.objtools records the origin and bounds Merged into existing extras. If the input already has an origin, the new centre is added to it. Otherwise re-importing a centred asset would lose its georeference.
--up-axis z rotates each mesh node The rotation is applied in a new parent above the scene roots. Applying it to a file already imported with it is an error. Nodes can be nested and can already have a rotation, and a second rotation would turn the model on its side.
Warning for a fully transparent material Only when alphaMode is BLEND. OPAQUE ignores alpha.

Not applied

OBJ behaviour Why it is not applied
UV V flip glTF's (0, 0) is already the upper-left corner of the image.
Smooth normals generated when NORMAL is missing The spec gives these primitives implicit flat normals.
COLOR_0 renamed _COLOR under a texture The spec defines COLOR_0 as an extra multiplier of the base colour, so the tint is intended.
alphaMode inferred from a transparency scan alphaMode is authored, and OPAQUE means any alpha value is ignored.
Phong to PBR conversion and sourcePhong glTF materials are already PBR.
One flat node per mesh The node hierarchy is kept.
Vertex welding The existing deduplication uses OBJ index tuples, which glTF doesn't have. glTF indices already define its vertices.

inspect reports a glTF as it is, so its face count can be higher than the count after import removes zero-area faces.

Center and up axis

The centre is computed in world space, across all nodes and instances. Each mesh contributes the box of its POSITION bounds as seen through its transform, because reading every vertex once per instance wouldn't scale. Under a rotation that box can be slightly larger than the geometry, which only moves the centre and the recorded bounds.

The whole model then takes the centre in one of two ways:

  • In POSITION, as on the OBJ path, which keeps precision in a float32 renderer. This needs every mesh at the world origin with nothing else moving with it: float32 positions that aren't sparse, no skins, animations or instancing, and only identity transforms, with no camera or light, on the way up from each mesh. Subtrees without meshes, such as lights, are shifted through their own root.
  • In a new parent node above the scene roots, in every other case. Existing nodes keep their indices and transforms.

The same new parent carries --up-axis z. A root in several scenes gets a parent of its own, since a node can have only one. A skinned mesh stays at the root, as the spec recommends: its node's transform is ignored and its joints carry the new parent's.

See Georeferenced coordinates for how to recover source positions.

Input that is rejected

Reserved for files that can't be interpreted, or that would silently give wrong geometry. All of it is checked before any output is published.

  • A required extension that isn't supported, or optional Draco without uncompressed data (details).
  • An unknown primitive mode, or no triangles at all.
  • A node with two parents, a hierarchy with a cycle, or a scene root that is another node's child.
  • A reference to something that doesn't exist.
  • A POSITION that isn't VEC3 with data, an attribute whose count differs from POSITION, or indices that aren't unsigned integer scalars or are out of range.
  • Instancing attributes with different counts.
  • A non-finite value in an accessor.
  • A malformed extras.objtools.origin, rather than a lost georeference.

Conversion · Documentation home

Clone this wiki locally