flutter3d
Showcase Changelog 38 packages API reference

Meshopt compression

since 0.7.0 Formats and I/O

EXT_meshopt_compression packs a mesh's vertex and index buffers into a smaller bitstream a reader has to know the extension to unpack. GltfWriter(compressGeometry: true) reaches for it; this page writes a sphere both ways and reads the compressed one back.

Step 1: Write it plain, and compressed #

The vertex codec delta-codes each attribute byte lane against the vertex before it; the index codec exploits the edge a triangle usually shares with one drawn a few places back. Both bitstreams live in meshopt_vertex_codec.dart and meshopt_index_codec.dart; GltfWriter is what reaches for them.

// The vertex and index bitstreams themselves live in
// `meshopt_vertex_codec.dart` and `meshopt_index_codec.dart`; this is
// the writer that reaches for them.
final plain = GltfWriter(_document).writeGlb();
final compressed = GltfWriter(_document, compressGeometry: true).writeGlb();
_plainBytes = plain.length;
_compressedBytes = compressed.length;

Step 2: Confirm the extension was actually used #

A GLB's JSON sits as plain text at the front of the file, so whether the writer actually reached for the extension is a search away rather than a guess from the byte count alone.

// A GLB's JSON chunk is plain UTF-8 text at the front of the file, so
// whether the extension was actually reached for is a byte search away.
_usedMeshopt = latin1
    .decode(compressed, allowInvalid: true)
    .contains('EXT_meshopt_compression');

Step 3: Read the compressed file back #

GltfLoader decodes EXT_meshopt_compression the same way it decodes an ordinary accessor, before anything downstream notices a difference. Reordering a mesh for the GPU's cache moves which byte offset a vertex lands at without changing what it draws, so the comparison below checks that the same triangles came back rather than that they are in the same order.

// Reordering moves which byte offset a vertex or an index lands at
// without changing what the mesh draws, so the comparison asks whether
// the same triangles came back, not whether they are in the same
// order.
_readBack = await GltfLoader().load(compressed);
_differences = compareModelDocuments(
  _document,
  _readBack,
  tolerance: 1e-2,
  allowVertexReorder: true,
);

Step 4: Check the claim #

The extension has to actually appear in the file, the geometry has to survive, and the compressed file has to be smaller than the plain one.

if (!_usedMeshopt) {
  throw StateError('compressGeometry did not reach for meshopt at all');
}
if (_differences.isNotEmpty) {
  throw StateError('the compressed mesh drew different triangles back');
}
if (_compressedBytes >= _plainBytes) {
  throw StateError('the compressed file was not smaller');
}
if (frame.drawCalls < 1) {
  throw StateError('the ball was not drawn');
}