flutter3d
Showcase Changelog 38 packages API reference

The .f3d container

since 0.1.0 Formats and I/O

glTF is fast to load because its buffers are already what a GPU wants. .f3d takes that further: it is this engine's own container, built so loading is almost no work at all, a header read and the rest handed back as views over the same bytes.

Step 1: Write it, and a GLB beside it #

F3dWriter is the offline half of the format, the one dart run flutter3d_build:convert runs once per model. This page also writes the same sphere as a GLB, only to put a number beside .f3d's own.

// `F3dWriter.write` is the offline half, run once by
// `dart run flutter3d_build:convert`. Nothing here is on a frame path.
final bytes = F3dWriter(_document).write();
_fileBytes = bytes.length;
_glbBytes = GltfWriter(_document).writeGlb().length;

Step 2: Read it back #

F3dDocument.parse reads the header and the section directory to find where things are. Nothing is decoded eagerly: surfaces, once asked for, is a Float32List view over the file's own bytes rather than a fresh copy.

// Parsing reads the header and the section directory; every array the
// engine asks for afterwards, `surfaces` included, is a view over these
// same bytes rather than a copy.
_decoded = F3dDocument.parse(bytes);

Step 3: Compare the two containers #

The numbers below are for the same geometry, written two different ways.

String _report() =>
    '.f3d bytes: $_fileBytes\n'
    '.glb bytes (for comparison): $_glbBytes\n'
    'surfaces: ${_decoded.surfaces.length}\n'
    'vertices: ${_decoded.vertexCount}\n'
    'triangles: ${_decoded.triangleCount}';

Step 4: Check that the round trip held #

compareModelDocuments reads the geometry back byte for byte, since .f3d is a binary format with no rounding to account for.

final differences = compareModelDocuments(_document, _decoded);
if (differences.isNotEmpty) {
  throw StateError(
    'the round trip through .f3d changed something: '
    '$differences',
  );
}
if (_decoded.surfaces.isEmpty) {
  throw StateError('the file decoded to no surfaces');
}
if (frame.drawCalls < 1) {
  throw StateError('the ball was not drawn');
}