How to convert STL to OBJ
- Read the STL bytes or textOpen a .stl file. The loader detects whether it is binary or ASCII; binary input is parsed as an 80-byte header, a uint32 count and fixed 50-byte facets, while ASCII input is tokenised on facet normal, outer loop and vertex keywords.
- Collect the facet soupEach triangle yields three independent corners and one facet normal. Because STL has no vertex sharing, a mesh of N triangles arrives with 3N separate corner entries, many of them numerically identical across neighbouring facets.
- Flatten to one geometryAll corners are copied into a single BufferGeometry limited to position and normal channels. No material, UV or group data exists in the source, and none is introduced here; the attribute short from binary records is ignored.
- Weld repeated cornersWhile the triangle count remains below 300,000, the builder welds vertices keyed on position and normal, folding identical corners into a shared pool and building an index buffer. Larger meshes are expanded without such an index.
- Write v, vn and f linesThe OBJ exporter emits a global v list, a vn list and triangular f records that reference them. It writes no vt lines, no .mtl file, no usemtl, no o or g names and no colour.
- Save the .obj fileThe finished text is returned as a downloadable OBJ. Face winding is preserved from the source facets, and degenerate zero-area triangles pass through as thin f records.
What happens when STL is converted to OBJ
Parsing the binary STL container
A binary .stl begins with an 80-byte header whose contents are unconstrained and which this reader ignores. Immediately after it sits a little-endian 32-bit unsigned integer giving the number of triangles. The remaining bytes divide into fixed 50-byte records: twelve 32-bit floats describe a facet normal followed by three vertices, and a final 16-bit attribute short completes the block. The loader reads the count, then walks exactly that many records, converting every coordinate triple into a position and every normal into a normal vector. Because the stride is constant, no delimiter scanning is needed and the attribute field is discarded. A truncated file whose byte length is not 84 plus 50 times the count is a common failure mode.
Reading the ASCII spelling of the same data
Textual STL represents identical geometry with keywords instead of fixed records. A solid opens the file, each facet normal line supplies one normal, an outer loop bracket encloses three vertex lines, and endloop, endfacet and endsolid close the nested blocks. The reader tokenises these words and collects three coordinate triples per facet, ignoring whitespace and case differences. It does not use the solid name as an identifier. Numeric values arrive as decimal strings and are parsed into floats, so precision depends on how many digits the writer kept. Both encodings feed the same downstream facet list; the choice of input encoding has no effect on how the OBJ is later assembled, only on how the source is decoded.
Why STL corners cannot be shared
STL has no index concept. Every triangle carries its own three corner coordinates explicitly, so a vertex touched by six facets appears six separate times in the file, with the same numbers repeated. This is why a binary mesh of N facets always contains 3N corner triples regardless of how smooth the underlying surface is. The facet normal is stored once per triangle rather than once per vertex. Assembling an OBJ therefore requires deciding which of those repeated corners are genuinely the same point. The welder compares both position and normal, so two facets meeting at a hard crease keep distinct entries while smoothly joined facets can share one, which is precisely the distinction an OBJ vn list needs to express.
A single geometry before export
Whatever the input encoding, the parsed corners are merged into one BufferGeometry whose channels are restricted to positions and normals. STL offers no colour, no UV coordinates, no groups and no material slots, and the converter does not invent any. Even a file whose 80-byte header mentions a program name contributes no metadata to the mesh. The attribute short carried by binary facets is deliberately dropped, since OBJ has no matching per-face field in this writer. With the geometry reduced to two flat arrays and an optional index, the exporter has exactly one structure to serialise, which keeps the emitted OBJ free of any object, group or shading declarations.
Welding keyed on position and normal
When the triangle total is under 300,000, the builder collapses the 3N corners into a deduplicated pool. Two corners merge only if both their coordinate triples and their normal vectors match, so a crease between two differently oriented facets is preserved rather than smoothed away. The result is an indexed BufferGeometry: a compact vertex array plus an index buffer whose every consecutive triple names one triangle. Above that threshold the geometry is expanded without an index buffer, meaning the OBJ v list keeps repeated corners instead of a shared pool. Either path still produces valid f records, but the small-mesh path yields a markedly shorter file because identical coordinates are written once.
What the OBJ exporter actually writes
The output is plain text containing only three record types. Each welded vertex becomes a v line with x, y and z; each distinct normal becomes a vn line; each triangle becomes an f line whose three entries take the form vertex-index double-slash normal-index. There are no vt texture-coordinate lines, so the double slash is intentional and the middle index slot stays empty. No .mtl companion is generated, no usemtl statement appears, and no o or g names label the mesh. Indices are 1-based, as the OBJ specification requires, and reference the single global v list. Colour is absent from both the source and the target, so nothing needs translating.
Winding, normals and degenerate facets
The order of the three corners in each f record reproduces the winding of the original STL facet, which determines which side the geometric normal points toward. STL stores a facet normal that some exporters compute inconsistently; here the vn lines come from the normals carried through the BufferGeometry, so a flipped shell keeps its flipped orientation rather than being silently corrected. Triangles whose three corners are collinear have zero area and a normal that may be a zero vector. Such degenerate facets are still written as f records with three duplicated or nearly duplicated indices. The converter does not remove them, because STL offers no marker that would let the reader distinguish an intentional thin facet from a corrupt one.
STL vs OBJ
| STL source | OBJ output | |
|---|---|---|
| Vertex storage | Each facet repeats its three corners, 3N entries | Global v list referenced by f indices |
| Record type | Binary 50-byte facets or ASCII facet blocks | ASCII v, vn and f text lines |
| Vertex normals | One normal per facet | vn vertex normals with f double-slash references |
| Indexing | None; unindexed facet soup | Weld by position and normal below 300,000 triangles |
| Texture channel | No UVs in the source | No vt lines emitted |
| Materials / colour | Unused 16-bit attribute short | No .mtl, usemtl, groups or colour |
| Face shape | Triangles only | Triangular f records of form v//vn |
| Unit metadata | Source omits any measure | Coordinates copied with no scale |
| Hierarchy / names | None beyond the 80-byte header | No o or g names, one mesh |
| File size driver | 50 bytes times the facet count | Text length of v, vn and f lines |
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
No appearance data exists on either side
Neither endpoint of this pair carries colour, textures or materials. Binary STL has only a 16-bit attribute short per facet, which many writers leave at zero and which this exporter ignores, and ASCII STL has no appearance keyword at all. The OBJ writer deliberately omits vt lines, .mtl sidecars, usemtl statements and group names, so the result is a bare geometry. Consequently no material translation is attempted and no texture is loaded. A user hoping to recover a colour scheme from the 80-byte header or the attribute field will find nothing, because the flattened mesh keeps only position and normal channels before a single v line is produced.
Index behaviour changes above 300,000 triangles
The deduplication pass is conditional. Below 300,000 triangles, corners are welded by position and normal and the OBJ references a compact shared v list through an index buffer, which keeps the text small. At or above that count the geometry is expanded without an index buffer, so repeated corners are written out again and the v list grows toward the original 3N entries even though f records remain valid. This is a size behaviour, not a quality change: coordinates and normals are identical either way. Very large shells can therefore produce an OBJ whose vertex section is far longer than a hand-optimised indexed mesh, purely because the welding shortcut was skipped.
Zero-area triangles pass through
An STL facet whose three corners are collinear or coincident encloses no area. Such records survive parsing and are emitted as OBJ f lines with three indices that may point at nearly identical vertices, and their stored normal can be a zero vector rather than a meaningful direction. The welder still compares position and normal, so they are not automatically culled. Downstream readers may report these as degenerate faces or ignore them during shading, but this converter does not repair, merge or delete them. The facet count in the source header therefore matches the number of f records in the output, including any zero-area ones, which matters when diagnosing a mesh that looks unexpectedly sparse.
Orientation is preserved, not corrected
The converter copies facet winding and stored normals as given. If an STL file declares a facet normal that disagrees with the orientation implied by its corner order, the OBJ vn lines inherit that stored direction rather than a recomputed geometric one. Flipped shells, inward-facing patches and inconsistent normals across a shared edge are therefore carried into the output unchanged. There is no global reorientation pass and no smoothing-group reconstruction. Users who need consistent outward normals must fix the source mesh first, because this pair performs a structural reindexing only. The double-slash f syntax ties each corner to one vn entry, so any upstream inconsistency becomes visible as shading seams.
Both endpoints leave coordinates unscaled
Both endpoints of this pair leave coordinates as bare numbers with no scale attached. The writer copies x, y and z verbatim into v lines without any multiplication, and neither a comment nor a header field records millimetres or inches. A part whose corners span 0 to 100 is written as those same numbers. Tools that later load the OBJ may assume millimetres by convention, but the file itself offers no evidence for that assumption. Because the conversion is a pure reindexing of identical coordinates, it neither introduces nor resolves any scale ambiguity; resizing has to be applied before conversion or by editing the v values afterwards.
Text output can outweigh the binary source
OBJ is a human-readable text format, so the exported file is usually larger than a compact binary STL of the same mesh even after welding. Every v, vn and f line carries decimal digits, separators and line endings, and the digits chosen for each float determine both precision and length. The writer serialises the coordinates it parsed, so an ASCII STL with few significant digits stays coarse while a binary float may be printed with more. There is no compression step and no binary OBJ variant here. Large meshes can produce substantial text files, and the f section grows with triangle count regardless of how many distinct vertices the weld pass recovered.
Frequently asked questions
Does the OBJ keep the STL facet normals?
Yes, but as vertex normals. Each distinct normal from the parsed facets becomes a vn line, and every f entry references one through the vertex-index double-slash normal-index syntax. Because welding keys on both position and normal, corners that share coordinates but sit on differently oriented facets keep separate vn entries, so hard creases survive. The per-facet normal from the STL header is not stored as a face attribute; it is folded into the vn list that the f records point at.
Why does the OBJ have fewer v lines than the STL had corners?
A binary STL facet always stores three explicit corners, so N triangles produce 3N coordinate triples with no sharing. When the mesh is below 300,000 triangles, the converter welds corners keyed on position and normal and builds a shared global v list plus an index buffer. Identical corners collapse into one v line, and f records reference those indices. Above that threshold welding is skipped and the v list stays closer to the original 3N entries.
Is a .mtl file or any texture coordinate written?
Such records are not written. The exporter emits only v, vn and f lines. There are no vt texture-coordinate records, which is why f entries use the double-slash form with an empty middle slot, and no .mtl sidecar or usemtl statement is produced. The source STL carries no UVs or materials anyway, and the flattened BufferGeometry keeps just position and normal channels. Object names, groups and colour are likewise absent from the output.
What happens to the 16-bit attribute field in binary STL?
The attribute short occupies the last two bytes of each 50-byte facet, but it has no defined meaning in this pipeline and is discarded during parsing. STL readers sometimes use it to pack per-facet colour, yet the OBJ writer here has no colour channel and no per-face field to receive such data. Since the geometry is flattened into one mesh with only positions and normals before export, there is nowhere to attach the value, so it simply does not appear in the .obj.
Can the converter read ASCII STL files too?
ASCII input is accepted. Textual STL spells facets out with solid, facet normal, outer loop, vertex, endloop, endfacet and endsolid keywords instead of fixed binary records. The loader tokenises those lines, gathers three coordinate triples and one normal per facet, and parses the decimal numbers into floats. From that point the geometry follows the same welding and OBJ-writing path as binary input. The solid name is not used as an identifier, and the input encoding does not change the output structure.
Are degenerate zero-area triangles removed?
No. A facet whose three corners are collinear encloses no area and may carry a zero-length normal, but it is still written as an f record referencing three indices. The welder compares position and normal and does not cull such triangles, and there is no repair pass that merges or deletes them. The number of f lines therefore matches the triangle count declared in the STL header, including degenerate facets, which can matter when a mesh renders with unexpected gaps.
Does the OBJ preserve the winding of each facet?
It does. The three corners of every f line are emitted in the same order as the original STL facet, so the geometric normal implied by that winding is unchanged. Stored normals are carried into vn lines rather than recomputed, meaning a facet whose declared normal contradicts its corner order keeps that inconsistency. There is no global reorientation or smoothing reconstruction; this conversion reindexes geometry and leaves orientation exactly as the source presented it.
Are the copied coordinates scaled during reindexing?
No scaling is applied. The x, y and z values from the STL corners are copied verbatim into v lines, and no comment or header field records millimetres, inches or anything else. A part whose corners span 0 to 100 is written as those same numbers. A downstream tool may assume millimetres by convention, but the .obj contains no such statement. Because the operation is a structural reindexing of identical floats, any resizing must be done before conversion or by editing the v values directly.
Related converters
All converters
Last updated: