A decoder of your own
Open the live demo · Read the source · View on GitHub
This engine knows glTF, OBJ, STL and its own .f3d. A studio's internal
format, or anything invented after this page was written, comes in
through ModelDecoder, one interface an application implements itself.
Step 1: A format this engine has never heard of #
Three numbers and nothing else: a width, a height, a depth. No glTF header, no OBJ directive, nothing any built-in reader would recognise.
/// A made-up format this engine has never heard of: one line, three
/// numbers, a box of that size. `ModelDecoder` is the whole plugin
/// boundary — a project's own studio format arrives exactly this way.
final class _BoxTextDecoder implements ModelDecoder {
const _BoxTextDecoder();
@override
bool handles(String fileName, Uint8List bytes) =>
fileName.toLowerCase().endsWith('.box');
@override
Future<ModelDocument> decode(
Uint8List bytes,
ModelLoadRequest request,
AssetUriResolver resolveUri,
) async {
final parts = utf8.decode(bytes).trim().split(RegExp(r'\s+'));
if (parts.length != 3) {
throw const FormatException('a .box file is three numbers: w h d');
}
final size = Vector3(
double.parse(parts[0]),
double.parse(parts[1]),
double.parse(parts[2]),
);
return PlainModelDocument(
surfaces: <ModelSurface>[
ModelSurface(mesh: CuboidShape(size: size).build()),
],
nodes: <ModelNode>[
ModelNode(surfaces: <int>[0]),
],
);
}
}
Step 2: Hand it in with the request #
ModelLoadRequest.decoders carries an application's own readers
alongside the file. They travel with the request rather than living in a
registry, because decoding runs on a background isolate and a registry
filled on the main one would be empty there.
// A byte-identical file this engine's own decoders would refuse:
// three plain numbers, no glTF, no OBJ header, nothing recognisable.
final bytes = Uint8List.fromList(utf8.encode('1.4 0.6 2.0'));
final request = ModelLoadRequest(
source: _BytesSource(bytes, 'crate.box'),
decoders: const <ModelDecoder>[_BoxTextDecoder()],
);
Step 3: Decode it #
An application's own decoders are tried first, by file name and by
magic, before this package's built-in glTF, OBJ, STL and .f3d readers
ever see the bytes — which is also how a project replaces a built-in
reader rather than only adding to the list.
// An application's own decoders are tried before this package's
// built-in ones, by file name and by magic — so a project can also
// replace a built-in reader, not only add to it.
_decoded = await decodeModelBytes(
request,
bytes,
request.source.resolveUri,
);
Step 4: Check the claim #
The file named a box 1.4 units wide, so that is what the decoded document's own bounds should say.
if (_decoded.surfaces.length != 1) {
throw StateError('the custom decoder did not produce one surface');
}
final bounds = _decoded.computeBounds();
final width = bounds.max.x - bounds.min.x;
if ((width - 1.4).abs() > 1e-6) {
throw StateError('the box did not come out the size the file named');
}
if (frame.drawCalls < 1) {
throw StateError('the crate was not drawn');
}