flutter3d
Showcase Changelog 38 packages API reference

glTF and GLB

since 0.1.0 Formats and I/O

glTF is the format most tools export to, and GLB is the same thing packed into one binary file: JSON, geometry, and images in a single blob. This page writes a small GLB with this engine's own writer, then reads it back with the loader to show what a real file decodes into.

Step 1: A real GLB to read #

Two surfaces, two materials, and a node each. Writing it here means this page needs no bundled asset, only the same writer the gltf-write page exercises directly.

// A real GLB, written by this engine's own `GltfWriter` so this page
// needs no bundled asset to read: two surfaces, one material each.
final source = PlainModelDocument(
  surfaces: <ModelSurface>[
    ModelSurface(
      mesh: CuboidShape(size: Vector3.all(0.9)).build(),
      materialIndex: 0,
    ),
    ModelSurface(
      mesh: SphereShape(segments: 32, rings: 16).build(),
      materialIndex: 1,
    ),
  ],
  materials: <SurfaceMaterial>[
    SurfaceMaterial(baseColor: Vector4(0.8, 0.4, 0.3, 1.0)),
    SurfaceMaterial(
      baseColor: Vector4(0.3, 0.6, 0.8, 1.0),
      metallic: 0.8,
      roughness: 0.3,
    ),
  ],
  nodes: <ModelNode>[
    ModelNode(surfaces: <int>[0]),
    ModelNode(surfaces: <int>[1], translation: Vector3(1.6, 0.0, 0.0)),
  ],
);
final bytes = GltfWriter(source).writeGlb();

Step 2: Decode it #

A GLB carries everything it needs in one file, so there is no sibling to resolve before decoding starts. GltfLoader.load reads the container, walks its accessors and hands back a GltfAsset: a document of surfaces, materials, nodes, and, when the file has them, lights and cameras.

// `GltfLoader.load` reads the container, walks its accessors and
// materials, and hands back a `GltfAsset`, whatever wrote the file.
_asset = await GltfLoader().load(bytes);

Note. The loader also understands .gltf, the JSON-and-siblings form, EXT_meshopt_compression, KHR_draco_mesh_compression, and the basic KHR_materials_unlit and KHR_lights_punctual extensions. This file needs none of that, so the loader's own ordinary path is what shows.

Step 3: Upload each surface, at the node that places it #

A decoded document is not yet anything a device can draw. Each surface's mesh becomes a DeviceMesh, its material's few numbers become an engine Material, and the node hierarchy says where each one sits.

// `GltfAsset` is a `ModelDocument`: a list of surfaces, each with an
// index into the document's materials, plus a node hierarchy that
// places them. This page uploads each surface by hand rather than
// through `ModelAsset`, to keep the loader itself the whole story; the
// `model-asset` page shows the upload path a real application uses,
// textures included.
final scene = Scene();
for (final ModelNode node in _asset.nodes) {
  for (final int surfaceIndex in node.surfaces) {
    final ModelSurface surface = _asset.surfaces[surfaceIndex];
    final SurfaceMaterial? material = surface.materialIndex == null
        ? null
        : _asset.materials[surface.materialIndex!];
    scene.add(
      MeshNode(
        DeviceMesh.upload(context.device, surface.mesh),
        Material(
          name: material?.name,
          baseColor: material?.baseColor ?? Vector4(0.8, 0.8, 0.8, 1.0),
          metallic: material?.metallic ?? 0.0,
          roughness: material?.roughness ?? 0.6,
        ),
        name: surface.name,
      )..setPositionFrom(node.translation),
    );
  }
}

A real application usually skips this step and calls ModelAsset.fromDocument instead, which also uploads textures and keeps meshes and images from being uploaded twice. The model-asset page shows that path.

Step 4: Check that both surfaces actually loaded #

A loader that silently drops a surface is worse than one that throws, because nothing downstream notices until the screen is missing an object. This page's own claim is that both surfaces came back and both were drawn.

if (_asset.surfaces.length != 2) {
  throw StateError('the GLB decoded to the wrong number of surfaces');
}
if (_asset.vertexCount == 0) {
  throw StateError('the decoded mesh has no vertices');
}
if (frame.drawCalls < 2) {
  throw StateError('both surfaces were not drawn');
}