OBJ and MTL
Open the live demo · Read the source · View on GitHub
Wavefront OBJ is plain text, older than glTF, and still what a lot of tools export by default. This page reads a tiny hand-written OBJ, fills in what it left out, and writes it back.
Step 1: A file with no normals #
Real OBJ files skip vn often enough that a loader has to have an answer
for it. This tetrahedron has four vertices and four faces, and nothing
else.
// A tetrahedron with no `vn` records at all — real files omit normals
// often enough that `ObjLoader` has to have an answer for it.
static const String _sourceObj = '''
v 0 1 0
v 1 -1 1
v -1 -1 1
v 0 -1 -1.4
f 1 2 3
f 1 3 4
f 1 4 2
f 2 4 3
''';
Step 2: Decode it, and let the loader fill the gap #
ObjNormals.smooth, the default, averages the face normals meeting at
each vertex rather than leaving them at zero. A flat-shaded look on a
curved surface is exactly what leaving normals at zero would produce.
// `ObjNormals.smooth`, the loader's default, averages the face normals
// meeting at each vertex rather than leaving them at zero.
final bytes = Uint8List.fromList(utf8.encode(_sourceObj));
_decoded = await ObjLoader(normals: ObjNormals.smooth).load(bytes);
Note.
ObjNormals.flatgives one normal per face instead, which needs shared vertices split apart first.ObjNormals.noneleaves them at zero, for a caller that computes its own.
Step 3: Write it back out #
ObjWriter takes any ModelDocument, not only one ObjLoader produced,
and writes the dialect the loader reads: the same V flip, the same sticky
usemtl. This document has no material, so only the .obj comes out; a
.mtl is written only when the document names one.
final writer = ObjWriter(_decoded, name: 'tetra');
_written = writer.write();
Step 4: Compare source and output #
The source had no normals; the file this page writes does, because a
normal that was generated to satisfy the loader's own request is still a
normal ObjWriter will write.
String _report() {
final mesh = _decoded.surfaces.single.mesh;
return 'source bytes: ${_sourceObj.length}\n'
'written bytes: ${_written.length}\n'
'vertices: ${mesh.vertexCount}\n'
'triangles: ${mesh.triangleCount}\n'
'a generated normal: ${mesh.vertices[3].toStringAsFixed(3)}, '
'${mesh.vertices[4].toStringAsFixed(3)}, '
'${mesh.vertices[5].toStringAsFixed(3)}';
}
Step 5: Check what the page claims #
Four faces in, four triangles out, and a generated normal that actually points somewhere rather than sitting at zero.
final mesh = _decoded.surfaces.single.mesh;
if (mesh.triangleCount != 4) {
throw StateError('the tetrahedron did not decode to four triangles');
}
// A generated normal is never the zero vector `ObjNormals.none` would
// have left behind.
final nx = mesh.vertices[3];
final ny = mesh.vertices[4];
final nz = mesh.vertices[5];
if (nx == 0.0 && ny == 0.0 && nz == 0.0) {
throw StateError('no normal was generated for a file that had none');
}
if (_written.isEmpty) {
throw StateError('the writer produced nothing');
}
if (frame.drawCalls < 1) {
throw StateError('the tetrahedron was not drawn');
}