flutter3d
Showcase Changelog 38 packages API reference

A decoder of your own

since 0.5.0 or earlier Formats and I/O

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');
}