glTF and GLB
Open the live demo · Read the source · View on GitHub
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 basicKHR_materials_unlitandKHR_lights_punctualextensions. 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');
}