OBJ and MTL: the source
Open the live demo · Read the guide · View on GitHub
Regions the guide quotes: source (line 19), load (line 34), write (line 39), report (line 57), check (line 86).
1/// Reading and writing Wavefront OBJ, with `ObjNormals` filling in what a
2/// file left out.
3///
4/// Quoted by `obj.md` and shown whole in the Source tab.
5library;
7import 'dart:convert';
8import 'dart:typed_data';
10import 'package:flutter/widgets.dart';
11import 'package:flutter3d/flutter3d.dart';
12import 'package:flutter3d_showcase/src/demo/demo.dart';
13import 'package:vector_math/vector_math.dart';
15final class ObjDemo extends ShowcaseDemo {
16 late final ModelDocument _decoded;
17 late final Uint8List _written;
19 // A tetrahedron with no `vn` records at all — real files omit normals
20 // often enough that `ObjLoader` has to have an answer for it.
21 static const String _sourceObj = '''
22v 0 1 0
23v 1 -1 1
24v -1 -1 1
25v 0 -1 -1.4
26f 1 2 3
27f 1 3 4
28f 1 4 2
29f 2 4 3
30''';
33 Future<void> prepare(DemoContext context) async {
34 // `ObjNormals.smooth`, the loader's default, averages the face normals
35 // meeting at each vertex rather than leaving them at zero.
36 final bytes = Uint8List.fromList(utf8.encode(_sourceObj));
37 _decoded = await ObjLoader(normals: ObjNormals.smooth).load(bytes);
39 final writer = ObjWriter(_decoded, name: 'tetra');
40 _written = writer.write();
41 }
44 Scene build(DemoContext context) => Scene()
45 ..add(
46 MeshNode(
47 DeviceMesh.upload(context.device, _decoded.surfaces.single.mesh),
48 Material(baseColor: Vector4(0.8, 0.6, 0.3, 1.0), roughness: 0.6),
49 name: 'tetra',
50 ),
51 )
52 ..add(
53 LightNode(name: 'sun', intensity: 3.0)
54 ..setLocalForward(Vector3(-0.4, -1.0, -0.3)),
55 );
57 String _report() {
58 final mesh = _decoded.surfaces.single.mesh;
59 return 'source bytes: ${_sourceObj.length}\n'
60 'written bytes: ${_written.length}\n'
61 'vertices: ${mesh.vertexCount}\n'
62 'triangles: ${mesh.triangleCount}\n'
63 'a generated normal: ${mesh.vertices[3].toStringAsFixed(3)}, '
64 '${mesh.vertices[4].toStringAsFixed(3)}, '
65 '${mesh.vertices[5].toStringAsFixed(3)}';
66 }
69 Widget? customBody(BuildContext buildContext, DemoContext context) =>
70 Container(
71 color: const Color(0xFF14161A),
72 padding: const EdgeInsets.all(24),
73 alignment: Alignment.topLeft,
74 child: DefaultTextStyle(
75 style: const TextStyle(
76 color: Color(0xFFE8E8EC),
77 fontSize: 16,
78 fontFamily: 'monospace',
79 ),
80 child: Text(_report()),
81 ),
82 );
85 void verify(Scene scene, FrameResult frame) {
86 final mesh = _decoded.surfaces.single.mesh;
87 if (mesh.triangleCount != 4) {
88 throw StateError('the tetrahedron did not decode to four triangles');
89 }
90 // A generated normal is never the zero vector `ObjNormals.none` would
91 // have left behind.
92 final nx = mesh.vertices[3];
93 final ny = mesh.vertices[4];
94 final nz = mesh.vertices[5];
95 if (nx == 0.0 && ny == 0.0 && nz == 0.0) {
96 throw StateError('no normal was generated for a file that had none');
97 }
98 if (_written.isEmpty) {
99 throw StateError('the writer produced nothing');
100 }
101 if (frame.drawCalls < 1) {
102 throw StateError('the tetrahedron was not drawn');
103 }
104 }
105}