How to convert OBJ to USDZ
- Load the OBJ textOpen the .obj file inside the page; the three.js OBJLoader tokenises every line, gathering v coordinate triples, vn normal triples, vt pairs and f index sets while ignoring comments and blank rows.
- Skip sidecar referencesTokens such as mtllib, usemtl, o, g and s are recognised yet not acted upon, so no .mtl file is fetched and no image is decoded before geometry construction begins.
- Flatten to one meshObject boundaries, group labels and smoothing flags collapse into a single BufferGeometry; each polygon is triangulated and only float32 position and normal arrays are retained for export.
- Compose the USD layersThe writer creates a root model.usda plus geometry files named geometries/Geometry_*.usda, declaring one Xform prim and one Mesh prim with their scalar and array properties.
- Build the ZIP containerThose USDA layers are inserted as stored entries with ordinary local file headers and a normal central directory, yielding a .usdz archive that unzip tools can read directly.
- Save the archiveThe single downloadable file holds no timeSamples, no skins, no materials, no textures, no cameras and no lights; only the static mesh description is serialised.
What happens when OBJ is converted to USDZ
Parsing the line-oriented OBJ grammar
Each OBJ statement occupies one text line and begins with a short keyword followed by space-delimited numbers. A v line contributes three or four coordinate components, vn supplies a normal triple, vt carries two or three texture values, and f lists face corners as slash-separated indices into those three tables. Positive face indices are one-based, while negative values count backwards from the most recently read vertex, a detail the decoder must resolve before any triangle exists. Quads and larger n-gons are legal, so the reader splits them into triangles. Hash-prefixed comments and empty lines are skipped. Because the format is pure ASCII, no binary header, byte order marker or length field needs interpretation, and malformed numeric tokens are rejected rather than silently reinterpreted.
Why .mtl sidecars and textures are ignored
An OBJ may name a companion material library through an mtllib statement and reference its entries with usemtl, but this converter never follows those pointers. The mesh loader treats the geometry stream as self-contained: it builds vertices, normals and faces without opening the referenced .mtl or decoding any map_Kd image. That choice matters for the target, because a USD stage would need a UsdPreviewSurface prim plus file-bound texture assets to reproduce shading, and none of that data survives parsing. Group records o and g, smoothing statements s, and per-face material switches are likewise reduced to nothing. Consequently the pipeline receives only numbers, and the flattened geometry arrives with no UV set and no material binding for the USDZ writer to translate.
Flattening into one BufferGeometry
Before serialisation every parsed object is merged into a single BufferGeometry. The merge unifies the three independent OBJ index domains into one vertex table: when a face corner references position 7 but normal 12, the builder either duplicates the position or keeps a shared normal so that one index addresses both attributes. Triangulation turns each f polygon into triples of corner indices. The resulting attribute set contains exactly two entries, a float32 position vector of three components and a matching float32 normal vector; vt data, vertex colours, groups and material indices are omitted. With no groups array and no draw ranges, the USDZ exporter sees one primitive rather than a collection, which is why the stage ends up with a single Mesh prim instead of a hierarchy of children.
Laying out the USDA layer files
The archive payload is authored as ASCII .usda rather than the compact binary crate format, so every property remains human-readable text. A root layer called model.usda establishes the stage, declares the default prim and references additional layers stored beneath geometries/Geometry_*.usda. Splitting geometry into referenced layers keeps the root small and lets a consumer compose the stage through ordinary USD layer stacking. Within the text, def statements introduce prims, uniform tokens fix interpolation, and array-valued attributes appear as bracketed lists of numbers. No .usdc crate file, no compressed payload and no external texture package is generated. Because the layers are plain text, a reader that only understands binary crates will not find the geometry it expects, which is one reason this writer sticks to the .usda spelling throughout.
Filling the UsdGeom Mesh attributes
A UsdGeom Mesh prim is described by properties that mirror the flattened buffer. point3f[] points receives the float32 position triples directly, int[] faceVertexCounts is filled with the value 3 for every triangle, and int[] faceVertexIndices stores the concatenated corner indices into that point array. normal3f[] normals is authored as a primvar with vertex interpolation so the shading vectors line up with the points length. There is no primvar for texture coordinates, no subset for face sets, no material binding and no UsdPreviewSurface prim. Extent is not authored as a separate attribute, and the Xform above the mesh carries no translation, rotation or scale operations. The result is a minimal, static, untextured surface that a USD stage can compose and display as one rigid object.
Assembling a compliant ZIP package
USDZ is a ZIP container with a specific packaging rule: member files are stored without deflate compression and the archive stays readable through the standard local-header plus central-directory mechanism. The writer adds model.usda and each geometry layer as stored entries, aligning data offsets so tools that memory-map the first member can locate it without scanning for a hidden signature. No prepended bytes, no disguised central directory and no compression method other than stored are used. A consumer that insists on a single binary crate layer, or that expects geometry inside a compressed member, will fail to compose the stage even though the bytes form a valid ZIP. Keeping the layout conventional is therefore part of producing a usable .usdz rather than an incidental detail.
OBJ vs USDZ
| OBJ source | USDZ output | |
|---|---|---|
| Feature | OBJ source | USDZ output |
| Container | Plain-text ASCII file, one record per line | Uncompressed ZIP holding USDA layers |
| Geometry carrier | v, vn, vt and f statements | UsdGeom Mesh prim properties |
| Triangle description | Face index sets, optionally n-gons | faceVertexCounts plus faceVertexIndices |
| Normals | vn triples, optional | normal3f[] normals primvar |
| Texture coordinates | vt pairs, optional | Absent; no UV primvar is written |
| Materials | Optional .mtl sidecar and usemtl | None; no material binding |
| Grouping | o and g names, smoothing groups | Single Xform and Mesh prim path |
| Scene graph | Flat list of records | Prims, properties and xforms |
| Units and axis | No unit or up-axis declaration | No upAxis or metersPerUnit metadata |
| Animation | Not representable | No timeSamples; static stage |
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 | – | 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
Group names and object partitions disappear
OBJ files frequently divide a model with o and g records, giving each subset a name and letting usemtl switch shading between faces. After parsing, the converter merges everything into one BufferGeometry with a single index buffer, so those partitions have nowhere to go in the USDZ. The stage therefore exposes one Xform prim and one Mesh prim rather than a tree of named children. Anyone relying on selecting a bolt, a panel or a wheel by its OBJ group label will find only an undifferentiated triangle set. Smoothing groups s are discarded as well, so shading discontinuities encoded as separate normal sets collapse into the single normal array. Rebuilding that structure would require re-segmenting the mesh, which this pipeline does not attempt.
UV coordinates and texture references never reach the stage
A vt line in the source supplies two or three values per corner, but the flattened geometry keeps only position and normal attributes, so the USDZ contains no primvars:st or UV set of any kind. Likewise the mtllib path to a .mtl library and any map_Kd image inside it are never opened, because the mesh loader does not follow sidecars. Even if texture bytes were present, a valid USDZ would need those images packaged as archive members plus a UsdPreviewSurface with file inputs, and this writer creates neither. The practical consequence is that assigning a texture after conversion requires rebuilding UVs on the target mesh. Colour values occasionally appended to v lines are also dropped, leaving a purely geometric surface.
No material, colour or shading network is bound
USD can express rich shading through Material prims, UsdPreviewSurface shaders and bound texture assets, yet the exported stage carries none of them. Because the flattening step strips material indices and the OBJ reader ignores usemtl, the writer receives a geometry object with no material attached at all. It reacts by emitting a warning about the missing MeshStandardMaterial and then writes an unshaded Mesh prim. Consumers will apply their own default surface appearance, which may look grey or flat and unlike the source intent. There is no base colour factor, no metallic or roughness value, no double-sided flag derived from the OBJ and no opacity setting. Any appearance work has to be redone downstream against the untextured geometry.
Unit and orientation metadata is absent
Neither OBJ nor the generated USDZ declares a physical scale. OBJ has no unit statement, and the converter does not write metersPerUnit or upAxis into the stage metadata, so raw coordinate numbers cross the boundary untouched. AR viewers that expect a Y-up or Z-up axis and metre-sized objects must guess, and a model built in centimetres or inches will be displayed at the wrong size. There is no automatic rescaling, no axis rotation and no coordinate-system conversion applied during export. Users who need a specific orientation or scale must adjust the source coordinates before conversion, because the tool performs no geometry transformation beyond triangulation and normal handling.
Time-varying and scene-level content is not representable
USD supports animation through timeSamples, skeletal bindings via UsdSkel, cameras, lights and variant sets, yet the OBJ input can express none of these and the exporter authors none of them. The output stage is a single static mesh with no time codes, no skeleton, no camera prim and no light prim. There is also no default prim rotation that would animate a turntable. If a downstream workflow expects a moving asset or a hierarchy of separately transformable parts, it receives a rigid, unanimated surface instead. This is a structural limit of reducing an OBJ text stream to one BufferGeometry, not a setting that can be toggled, and no skin weights or morph targets are reconstructed.
Frequently asked questions
Will the USDZ contain my .mtl file and its textures?
No. The OBJ loader treats mtllib and usemtl as inert tokens and never opens the referenced library, so no .mtl or image bytes enter the pipeline. The flattened mesh also lacks UV coordinates, which a bound texture would require. The archive therefore holds only model.usda and the geometries/Geometry_*.usda layers, all as uncompressed text. If surface imagery is needed, it has to be authored again against the converted mesh rather than recovered from this conversion.
Why does the exported model have no colour?
Colour can enter an OBJ either through a usemtl reference or as extra numbers trailing a v line, and both routes are dropped here. The converter keeps only position and normal attributes when it builds the single BufferGeometry, and the USDZ writer binds no Material prim. Without a MeshStandardMaterial on the incoming geometry the writer even emits a diagnostic before producing an unshaded Mesh. Viewers will show their own default appearance, so any tint must be assigned after conversion.
Do OBJ group and object names survive the conversion?
They do not. Records beginning with o or g are parsed but their labels are discarded once every face is merged into one index buffer. The resulting stage contains a single Xform prim above a single Mesh prim, with no child prims carrying those names. Selecting an individual component by its original group label is therefore impossible from the USDZ alone. Smoothing group statements s are removed at the same flattening step, so only a unified normal array remains.
Which up axis and unit scale does the USDZ use?
The converter writes neither upAxis nor metersPerUnit into the stage metadata, and OBJ itself declares no unit or handedness, so coordinate values are copied across verbatim. AR viewing environments commonly assume a Y-up or Z-up convention along with metre-scale dimensions, and they fall back to their own defaults here. A model measured in millimetres will consequently appear far too large. No axis rotation or rescaling is applied, so adjust the source numbers beforehand if a particular orientation matters.
Is the .usdz archive compressed?
No. Members are stored without deflate compression, which is part of the USDZ packaging convention, and geometry is written as human-readable ASCII .usda layers rather than binary crate files. The writer keeps a normal central directory and regular offsets, so ordinary ZIP tooling can list and extract model.usda plus the geometry layers directly. A reader expecting compressed members or a single .usdc layer will not compose the stage, even though the container itself is a completely valid ZIP archive.
What USD prim types appear inside the layers?
The root model.usda declares one Xform prim that acts as a parent and one UsdGeom Mesh prim beneath it. The Mesh carries point3f[] points, int[] faceVertexCounts filled with threes, int[] faceVertexIndices for the corner indices, and a normal3f[] normals primvar with vertex interpolation. There is no Material prim, no UsdPreviewSurface, no camera and no light. Referenced geometry layers live under geometries/Geometry_*.usda, and the whole stage stays static with no timeSamples.
Can the USDZ hold animation or several frames?
It cannot, for this conversion. OBJ describes one static set of vertices, and the exporter reduces that to a single BufferGeometry before authoring the stage, so there are no timeSamples, no skinning and no morph targets to serialise. USD does support animated properties in general, but nothing here generates them. The archive also has no skeleton or variant set. Any motion or frame sequence must be added later with a tool that edits USD stages.
What happens to quads and n-gons in the OBJ?
The loader triangulates every f polygon, so a quad becomes two triangles and a larger n-gon is split into a fan of triangles. That is why faceVertexCounts in the USDZ is uniformly 3 rather than a mix of threes and fours. Negative, one-based and separate position, normal and texture indices are all resolved into one shared vertex table beforehand. The converted mesh keeps the same silhouette but no longer records the original polygon boundaries.
Related converters
All converters
Last updated: