The .f3d container
Open the live demo · Read the source · View on GitHub
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');
}