flutter3d
Showcase Changelog 38 packages API reference

Writing GLB

since 0.7.0 Formats and I/O

GltfWriter turns any ModelDocument, however it was decoded, into a self-contained GLB. This page writes the same sphere twice, once plain and once with compressGeometry, and compares the two files it gets back.

Step 1: A document with something worth compressing #

A sphere with forty-eight segments has enough vertices that reordering them for the GPU's cache and packing their attributes into smaller types actually saves bytes. A cube would be too small to show the difference.

// A sphere with enough vertices that quantizing its attributes and
// reordering its triangles actually has something to save on.
final document = PlainModelDocument(
  surfaces: <ModelSurface>[
    ModelSurface(
      mesh: SphereShape(segments: 48, rings: 24).build(),
      materialIndex: 0,
    ),
  ],
  materials: <SurfaceMaterial>[
    SurfaceMaterial(
      name: 'shell',
      baseColor: Vector4(0.8, 0.5, 0.2, 1.0),
      roughness: 0.5,
    ),
  ],
  nodes: <ModelNode>[
    ModelNode(surfaces: <int>[0]),
  ],
);

Step 2: Write it twice #

GltfWriter.writeGlb is the whole encode: geometry, the one material, and the header around them. compressGeometry: true asks for two independent things on the way out, reordering the mesh for the GPU's post-transform cache and packing normals and texture coordinates as normalized integers instead of floats.

final plain = GltfWriter(document).writeGlb();
final compressor = GltfWriter(document, compressGeometry: true);
final compressed = compressor.writeGlb();

Note. compressGeometry never touches POSITION. Making that safe needs a per-mesh dequantization scale baked into every node that draws the mesh, and a node can draw more than one surface without a box they all share, so this writer leaves positions as plain floats rather than quantize them incorrectly.

Step 3: Read the numbers back #

GltfWriter says, after writing, whether the compression actually did anything: usedGeometryQuantization and usedVertexCacheReordering are both false only when a document has nothing for them to change.

String _report() {
  final magicText = String.fromCharCodes(_magic);
  final ratio = _plainBytes / _compressedBytes;
  return 'GLB magic: "$magicText"\n'
      'plain:      $_plainBytes bytes\n'
      'compressed: $_compressedBytes bytes '
      '(${ratio.toStringAsFixed(2)}x smaller)\n'
      'used quantization: $_usedQuantization\n'
      'used vertex-cache reordering: $_usedReordering';
}

Step 4: Check the file, not just the byte count #

A smaller file that starts with the wrong bytes is not a GLB at all. This page's claim is three things at once: the file starts with glTF's own magic, the compressed file is smaller than the plain one, and the sphere that both describe actually got drawn.

if (_magic.first != 0x67) {
  throw StateError('the file does not start with the GLB magic');
}
if (_compressedBytes >= _plainBytes) {
  throw StateError('compressGeometry did not shrink the file');
}
if (frame.drawCalls < 1) {
  throw StateError('the sphere was not drawn');
}