cadtomesh

GLB → OBJ

Convert GLB Scenes into a Single OBJ Mesh

GLB packs a glTF 2.0 scene into a 12-byte header, a JSON chunk and a BIN chunk. The converter reads the JSON, applies each node transform, merges every primitive into one BufferGeometry, then writes a single .obj of v, vn and f records. Materials, base colour factors, textures, skinning, animations and UVs are dropped, and no .mtl sidecar is produced.

Drop a .glb file here, or

Processed on this device ยท nothing uploaded

16input formats
7output formats
0 Buploaded, ever

How to convert GLB to OBJ

  1. Validate the GLB headerRead the first twelve bytes, confirm magic 0x46546C67 and version 2, and use the stored length to bound parsing of the two chunks that follow.
  2. Split JSON and BIN chunksEvery chunk carries a 4-byte length and a 4-byte type; 0x4E4F534A marks JSON and 0x004E4942 marks the binary buffer, both padded to four-byte boundaries.
  3. Resolve the scene graphWalk scenes, nodes and their children, composing matrix or translation, rotation and scale values into world transforms for each referenced primitive.
  4. Merge primitivesDecode POSITION, NORMAL and indices through accessors and bufferViews, transform the vertices, offset concatenated index ranges, and combine everything into one BufferGeometry.
  5. Emit OBJ recordsWrite one .obj with v position lines, vn normal lines and triangulated f face lines using 1-based indices, with no vt, mtllib, usemtl, o or g statements.

What happens when GLB is converted to OBJ

Reading the container header and chunks

Decoding starts at byte zero, where three little-endian uint32 values occupy twelve bytes. The magic 0x46546C67 identifies a glTF binary, the version field must equal 2, and the length field states how many bytes the whole asset spans. Beyond that point the file is a sequence of chunks, each introduced by a uint32 chunkLength and a uint32 chunkType. The JSON chunk tagged 0x4E4F534A is padded with spaces to a four-byte boundary, while the BIN chunk tagged 0x004E4942 is padded with zeros. Padding belongs to chunkLength, so the parser trims it before interpreting the text or the binary. A mismatch in magic, version or declared length aborts the load rather than producing partial geometry.

Interpreting the JSON scene description

The decoded text is a glTF 2.0 document whose top-level arrays describe the asset. scenes lists the default scene index, nodes enumerates transforms and child references, and meshes groups primitives that point at attribute accessors. accessors attach a componentType, a type such as VEC3 or SCALAR, a count, a byteOffset and min or max bounds to a bufferView, while bufferViews annotate byteOffset, byteLength, byteStride and target within the single buffer. That buffer omits any uri because its bytes live in the BIN chunk. The loader uses these declarations to locate POSITION, NORMAL and indices, then reads the corresponding byte ranges into typed arrays for every primitive it intends to keep.

Accessors, bufferViews and typed arrays

A POSITION accessor normally declares componentType 5126 for 32-bit floats and type VEC3, with min and max arrays describing the bounding box. NORMAL uses the same float layout, and the index accessor switches to componentType 5123 or 5125 with type SCALAR for unsigned short or unsigned int values. bufferView entries supply the byteStride when attributes are interleaved and a target of 34962 for arrays or 34963 for indices. The loader honours accessor byteOffset on top of bufferView byteOffset, converts little-endian bytes into JavaScript Float32Array or Uint16Array views, and treats index values as zero-based references into the vertex array. TEXCOORD_0 and COLOR_0 accessors are read only if present, then discarded during flattening because the OBJ target has no channel for them.

Composing node transforms

A glTF node may carry a 16-float column-major matrix or separate translation, rotation quaternion and scale triplets. The loader composes those local values with every ancestor along the node hierarchy, multiplying parent by child to obtain a world matrix for each mesh. POSITION triples are multiplied by that matrix so parts land in a shared coordinate frame. NORMAL vectors are transformed by the inverse transpose of the upper 3x3 block, which keeps shading correct under non-uniform scale, and are renormalised afterwards. Once baked, the transforms leave no trace in the output, since OBJ has no node concept. Nodes that reference cameras, lights or skins are skipped because they contribute no triangles to the merged surface.

Merging multiple meshes and primitives

A source GLB may contain several meshes, each with several primitives, and a primitive can declare a mode value other than a plain triangle list; such modes are expanded into ordinary triangles before concatenation. The loader then merges all vertex positions, normals and indices into a single BufferGeometry. Because glTF indices are zero-based and local to their own primitive, every appended batch has an offset equal to the running vertex count added to its index values. Nothing is deduplicated across primitive boundaries, so coincident vertices stay separate. Groups, names and per-primitive materials are not carried into the merge. The result is one flat triangle set that the OBJ writer can emit with a single continuous 1-based numbering.

Dropping appearance and animation data

glTF appearance lives in materials with pbrMetallicRoughness base colour factors, metallic and roughness values, plus images, samplers and textures that may be embedded in the BIN chunk. All of it is ignored here: the flattened mesh keeps only position and normal attributes, so base colour factors, texture coordinate sets and normal maps have no destination. skins, animations, morph target weights and cameras are likewise skipped. No .mtl library is written and no texture file is extracted from the binary buffer. The exporter simply walks the merged triangles and prints geometric records. Extension dictionaries such as extensionsUsed or extensionsRequired are consulted only to decide whether the geometry can be decoded at all.

GLB vs OBJ

GLB sourceOBJ output
FeatureGLB sourceOBJ output
ContainerBinary header plus JSON and BIN chunksPlain ASCII text, one record per line
Scene structurescenes, nodes, meshes, primitivesFlat list of v, vn and f records
TransformsNode matrices and TRS valuesBaked into vertex coordinates
Vertex attributesPOSITION, NORMAL, optional TEXCOORD_0v and vn lines only
IndexingZero-based unsigned index accessor1-based f corner references
MaterialspbrMetallicRoughness and texturesNone; no .mtl sidecar
CompressionPossible Draco, KTX2 or meshoptUncompressed decimal numbers
AnimationSkins and animation channelsNot representable
UnitsNo unit declaration writtenNo unit or scene graph
Output filesOne self-contained binaryOne .obj file, no sidecars

Supported formats

FormatReadWriteNotes
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)YesYesBinary and ASCII triangles, no units
OBJ (.obj)YesYesv / vn / f records; .mtl references ignored
PLY (.ply)YesYesBinary position and normal arrays
GLB / glTF (.glb .gltf)YesYesglTF 2.0, binary or JSON with data URI
3MF (.3mf)YesYesOPC/ZIP package, declares millimetres
FBX (.fbx)Yes–Binary and ASCII, static mesh only
DAE (.dae)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)YesYesZIP containing a USD stage

Known limitations and why a conversion can fail

No .mtl sidecar or extracted textures

A glTF material may include a base colour factor, metallic and roughness scalars, and references to images stored as bufferViews in the BIN chunk. This converter reads none of that appearance data into the merged geometry, so the OBJ writer has nothing from which to build an .mtl library. No mtllib statement appears and no bitmap is extracted from the binary buffer. The output is deliberately one .obj file alone. Anyone expecting a companion material library or texture images alongside it will not find them, and shading must be recreated from scratch against the flattened triangles.

UV coordinates and colour attributes vanish

TEXCOORD_0 accessors and COLOR_0 attributes are legitimate parts of a glTF primitive, but the intermediate BufferGeometry is reduced to position and normal before export. Consequently the OBJ contains no vt lines and no vt indices inside its f records, and per-vertex colours appended to v lines are not emitted either. Face statements reference only a position and a normal index. A texturing workflow that depends on the original UV layout cannot continue from this file without recomputing coordinates. Colour information is gone as well, leaving a purely geometric surface described by coordinates, normals and triangle connectivity.

The OBJ has no scene graph or unit declaration

glTF organises content through a hierarchy of nodes with transforms, and the loader bakes every one of those transforms into vertex coordinates during flattening. The resulting OBJ is a flat list of records with no o or g grouping and no way to express a parent transform. There is also no unit statement, so glTF metre assumptions are not translated into any scale annotation. Selecting a previously separate mesh by name becomes impossible, and moving one part relative to another is no longer a simple node edit. Hierarchical structure is lost at the merge and cannot be recovered from the text alone.

Compressed extensions are not decoded

A GLB may list KHR_draco_mesh_compression, KHR_texture_basisu for KTX2 images, or EXT_meshopt_compression in its extensionsUsed and extensionsRequired arrays. This converter does not implement those decoders, so geometry that depends on them cannot be reconstructed into raw position and normal arrays. When such an extension is marked required, the load fails rather than silently returning an empty mesh. Texture-oriented extensions are irrelevant to the untextured OBJ target, yet their presence can still block parsing if the primitive references them. Feeding an uncompressed glTF 2.0 GLB avoids this failure mode entirely, since the accessors then point at ordinary float and integer buffers.

Appearance, skinning and animation are lost

Beyond materials, a glTF asset can carry skins with joint matrices, inverse bind matrices and animation channels that move nodes over time. The merger keeps only static triangles, so skin weights, joint indices, morph targets and every animation sampler are discarded. Node transforms are baked once into vertex positions and never expressed as keyframes. The resulting OBJ is a rigid snapshot with no notion of time or deformation. Recreating articulated motion would require rebuilding a skeleton and re-rigging the flattened geometry in a separate modelling step, which this pipeline does not perform.

Frequently asked questions

Is an .mtl file or any texture written out?

No. Materials, base colour factors and texture references are ignored when primitives are merged into the single BufferGeometry, so the OBJ writer has no appearance data to serialise. The output contains only v, vn and f records with no mtllib statement, and no image is extracted from the BIN chunk. You receive one .obj file and nothing else. Shading or texture work must be started again on the flattened mesh after conversion.

What happens to node matrices and scales?

Each node matrix or translation, rotation and scale triplet is composed with its ancestors to form a world transform. POSITION triples are multiplied by that matrix, and normals are transformed by the inverse transpose of its upper 3x3 block then renormalised. Once baked, the values are plain coordinates in the OBJ and no transform record survives. A non-uniform scale is therefore reflected in vertex positions rather than stored as a separate operation. OBJ has no node hierarchy to hold such data.

Are UV coordinates preserved in the f records?

They are not. TEXCOORD_0 accessors are read only if present and then dropped during flattening, because the target keeps position and normal attributes alone. Face statements consequently carry position and normal references but no vt component, and no vt line appears anywhere in the file. Texture mapping cannot be resumed from this OBJ without generating a new UV layout. The same reduction removes any per-vertex colour attributes from the source primitive.

How are several glTF primitives combined?

Triangle strips and fans are expanded into plain triangles, and each primitive contributes its decoded positions, normals and indices to one BufferGeometry. Because glTF indices are zero-based and local to their primitive, an offset equal to the running vertex count is added to every appended index value. Vertices are not deduplicated across boundaries, so coincident points remain separate. The merged set is then written with continuous one-based numbering, which OBJ requires, rather than the zero-based indexing used inside the GLB.

Can the converter read Draco or KTX2 compressed GLB files?

No. Decoders for KHR_draco_mesh_compression, KHR_texture_basisu and EXT_meshopt_compression are not implemented, so primitives relying on them cannot be turned into raw float arrays. If such an extension appears in extensionsRequired, the load fails instead of returning partially decoded geometry. An uncompressed glTF 2.0 GLB with ordinary accessors and bufferViews is the form this route handles. Texture-oriented extensions add no value here anyway, since the OBJ target carries no images.

Why does the OBJ have no colour or material names?

The merged BufferGeometry retains only position and normal attributes, so base colour factors, material names and texture assignments have no destination in the output. The writer emits v, vn and f records exclusively, with no usemtl or mtllib statements and no mtl library beside the file. A viewer will apply its own default surface appearance. Recovering the original colours means assigning new materials to the flattened triangles in a downstream editor.

Does glTF metre scaling carry into the OBJ?

No. The GLB written by this site carries no unit declaration, and the OBJ target has no unit field either, so coordinate numbers pass through the transform bake without any scale annotation. glTF is conventionally read as metres with a Y-up axis, but nothing in either file states that. A viewer may assume metres and display the model at an unexpected size. No rescaling or axis conversion is applied during export, so size has to be corrected in the source or afterwards.

Are skins, morph targets or animations exported?

None of them are. Skins with joint and inverse bind matrices, morph target deltas and animation channels are all discarded when primitives are flattened into static triangles. Node transforms are baked once into vertex coordinates and never written as keyframes. The resulting OBJ describes a rigid surface with no concept of time or skeletal deformation. Rebuilding articulated motion requires re-rigging the merged geometry in a separate modelling tool after conversion.

Related converters

All converters

Last updated: