# Model formats: OBJ, STL, PLY, glTF No mesh-format module ships with pperl, and there is no maintained CPAN one to fall back on. That is less of a problem than it sounds: OBJ and STL are small enough to parse directly, and this chapter gives working readers and writers for both. ```{index} single: model file format ``` ## What the four differ on | | Geometry | Materials | Scene tree | Animation | Encoding | Indexed | |---|---|---|---|---|---|---| | OBJ | yes | via `.mtl` | no | no | text | yes | | STL | yes | no | no | no | text + binary | no | | PLY | yes | vertex colour | no | no | text + binary | yes | | glTF | yes | PBR | yes | yes | JSON + binary | yes | OBJ for interchange of static geometry, STL for 3D printing, PLY for scanned point and vertex data, glTF for anything with a scene, materials or animation. ```{index} single: OBJ single: Wavefront OBJ single: OBJ; 1-based and negative indices single: fan triangulation single: MTL ``` ## OBJ Line-oriented text. `v` a position, `vt` a texture coordinate, `vn` a normal, `f` a face referring to them. ```perl sub write_obj { my ($path, $v, $i) = @_; open my $fh, '>', $path or die "$path: $!"; print $fh "# cube, side 2\n"; printf $fh "v %g %g %g\n", @$_ for @$v; for (my $k = 0; $k < @$i; $k += 3) { # OBJ indices are 1-BASED printf $fh "f %d %d %d\n", map { $_ + 1 } @{$i}[$k, $k+1, $k+2]; } close $fh; } ``` The cube becomes 21 lines: a comment, 8 positions, 12 faces. ```text # cube, side 2 v -1 -1 -1 v 1 -1 -1 v 1 1 -1 ... f 1 6 5 ``` Reading is where the traps are, and there are four: ```perl sub read_obj { my ($path) = @_; open my $fh, '<', $path or die "$path: $!"; my (@v, @f); while (my $line = <$fh>) { next if $line =~ /^\s*(#|$)/; my @tok = split ' ', $line; my $kw = shift @tok; if ($kw eq 'v') { push @v, [ @tok[0..2] ] } elsif ($kw eq 'f') { # A face may have MORE than three vertices. Fan-triangulate. # Each token may be v, v/vt, v//vn or v/vt/vn: take the first. my @vi = map { my ($n) = split m{/}, $_; $n } @tok; # Indices are 1-based; a NEGATIVE index counts back from the # current end of the vertex list. @vi = map { $_ < 0 ? @v + $_ : $_ - 1 } @vi; push @f, [ $vi[0], $vi[$_], $vi[$_ + 1] ] for 1 .. $#vi - 1; } } close $fh; return (\@v, \@f); } ``` 1. **Indices are 1-based.** Off-by-one here is the most common OBJ bug. 2. **A negative index is relative**, counting back from the vertices read so far, which is how generators emit streaming output. 3. **A face can have any number of vertices.** Quads are common and n-gons occur; both need triangulating. Fan triangulation is correct for convex faces only, which in practice OBJ faces are. 4. **A face token has up to three slots**, `v/vt/vn`, and `vt` may be empty as `v//vn`. Splitting on `/` and taking the first field is enough when only positions are wanted. Round-tripping the cube reads back 8 vertices and 12 triangles, and a single 4-vertex face fans correctly: ```text read 8 vertices, 12 triangles one 4-vertex face -> 2 triangles: [0 1 2] [0 2 3] ``` **Symptom: a loaded model is missing every second triangle, or shows stray triangles connecting distant parts.** Cause: n-gon faces triangulated as if they were triangles, or the 1-based offset applied twice. Materials live in a sibling `.mtl` file named by a `mtllib` line, with `usemtl` switching material between faces. Colours, texture map filenames and shininess go there; geometry never does. ```{index} single: STL single: STL; binary and ASCII variants single: pack; fixed-size binary records ``` ## STL The 3D-printing format. Binary STL is a fixed-size record layout, which makes it the clearest `pack` exercise in this guide: ```perl sub write_stl { my ($path, $v, $i) = @_; open my $fh, '>', $path or die "$path: $!"; binmode $fh; # REQUIRED: binary format print $fh "\0" x 80; # 80-byte header, ignored print $fh pack "V", @$i / 3; # uint32 triangle count for (my $k = 0; $k < @$i; $k += 3) { my ($a, $b, $c) = @{$v}[ @{$i}[$k, $k+1, $k+2] ]; my $n = v_norm(v_cross(v_sub($b,$a), v_sub($c,$a))); # 12 little-endian floats then a uint16: 50 bytes per triangle. print $fh pack "f<12 v", @$n, @$a, @$b, @$c, 0; } close $fh; } ``` The size is completely determined: 80 header bytes, 4 count bytes, then 50 per triangle. ```text file size: 684 bytes (84 + 50 * 12 = 684) ``` Reading is the same layout in reverse, and the result shows STL's defining property: ```text read 12 triangles vertex records: 36, distinct positions: 8 ``` **STL has no index buffer.** Every triangle carries its three positions in full, so a cube's 8 corners arrive as 36 vertex records. Weld them (the [meshes chapter](meshes) has the code) before doing anything that needs adjacency, or the manifold check will report every edge as used once. Four more facts worth having: - The format is **little-endian** regardless of host. - The per-triangle **normal is frequently zero or wrong**. Recompute it from the winding rather than trusting it. - The trailing **uint16 is nominally an attribute byte count** and is normally 0. Some tools abuse it to store colour; there is no standard. - An **ASCII variant** exists (`solid`/`facet normal`/`outer loop`), equivalent in content and larger on disk. How much larger depends entirely on coordinate precision: this cube, whose coordinates are single digits, comes out 1471 bytes against 684, a factor of 2.2, while a scanned model with six significant figures per axis runs far higher. Detect the variant by checking whether the file begins with `solid` **and** its size does not match `84 + 50 * n`; testing the first five bytes alone is not enough, because a binary file's 80-byte header can begin with them too. ```{index} single: PLY ``` ## PLY Header-driven: the header declares the elements and their properties, and the body follows that declaration. ```text ply format ascii 1.0 element vertex 8 property float x property float y property float z element face 12 property list uchar int vertex_indices end_header ``` The parser reads the header, builds a record description from it, then reads that many records. That indirection is the point: PLY carries arbitrary per-vertex properties, so scanner output with confidence values, colours and normals per point needs no format extension. Both ASCII and binary bodies are declared by the `format` line. ```{index} single: glTF single: GLB single: glTF; accessors ``` ## glTF The modern interchange format, and the one to target for anything with structure. A `.gltf` is JSON describing nodes, meshes, materials, cameras and animations, referring to binary buffers for the bulk data; a `.glb` packs the same JSON and buffers into one file. Reading it fully is a project rather than a page, but the shape is worth knowing because it explains the design: `accessors` describe typed views into `bufferViews`, which are ranges of `buffers`. Vertex positions, indices and animation keyframes are all accessors over the same byte range mechanism, so a loader implements the accessor logic once. glTF is where PBR material parameters, a scene hierarchy and skinned animation actually live. OBJ and STL carry none of those, and no amount of convention makes them. ## Choosing - Writing geometry for another tool to read: **OBJ**, unless it needs materials or a hierarchy. - Sending a solid to a printer or a slicer: **STL**, welded on the way back in. - Point clouds or per-vertex measured data: **PLY**. - A scene, materials, or animation: **glTF**, and nothing else. ## Next Geometry can now be stored and loaded. The [next chapter](modelling-operations) constructs it: extrude, revolve, subdivision and boolean operations.