flutter3d
Showcase Changelog 38 packages API reference

Cameras and lights from a file

since 0.7.0 Formats and I/O

glTF can carry more than geometry. KHR_lights_punctual puts a light on a node the same way a mesh sits on one, and camera does the same for a viewpoint an author set up ahead of time. This page writes both into a GLB and reads them back to show what survives.

Step 1: Build a document with a light and a camera #

A ModelLight and a ModelCamera are held on ModelDocument next to the surfaces, addressed from a node by index the same way a mesh is. This document has one cube, one directional light, and one perspective camera.

// glTF's own `KHR_lights_punctual` and `camera` are held on a document as
// `ModelLight` and `ModelCamera`, addressed from a node the same way a
// surface is. This document has one of each, plus a cube to see by.
final document = PlainModelDocument(
  surfaces: <ModelSurface>[
    ModelSurface(mesh: CuboidShape(size: Vector3.all(1.0)).build()),
  ],
  lights: <ModelLight>[
    ModelLight(
      type: ModelLightType.directional,
      color: Vector3(1.0, 0.95, 0.85),
      intensity: 2.4,
    ),
  ],
  cameras: <ModelCamera>[
    const ModelCamera(
      projection: ModelPerspectiveCamera(yfov: 0.9, znear: 0.1),
      name: 'framing',
    ),
  ],
  nodes: <ModelNode>[
    ModelNode(surfaces: <int>[0]),
    ModelNode(name: 'sun', lightIndex: 0),
    ModelNode(name: 'framing', cameraIndex: 0),
  ],
);

Note. ModelCamera.projection is a perspective or an orthographic camera, glTF's own two kinds. Nothing here reads aspectRatio: when a file omits it, the camera is meant to follow whatever the viewport's aspect ratio is, which is what a null value says.

Step 2: Write it out and read it back #

GltfWriter encodes the whole document, lights and camera included, into one GLB. Reading it back with GltfLoader is the only honest way to check that the two agree on the shape of the file.

final bytes = GltfWriter(document).writeGlb();
_readBack = await GltfLoader().load(bytes);

Step 3: Draw what came back #

The cube and the light both came from the file, not from the document this page started with. The camera is not drawn; a camera has no shape of its own to show, only a point of view, and this page's own viewport already supplies one.

final scene = Scene();
for (final ModelSurface surface in _readBack.surfaces) {
  scene.add(
    MeshNode(
      DeviceMesh.upload(context.device, surface.mesh),
      Material(baseColor: Vector4(0.7, 0.75, 0.82, 1.0), roughness: 0.6),
      name: 'cube',
    ),
  );
}
for (final ModelLight light in _readBack.lights) {
  scene.add(
    LightNode(name: light.name, intensity: light.intensity)
      ..setLocalForward(Vector3(-0.4, -0.9, -0.3)),
  );
}

Step 4: Check that the round trip held #

A file that writes a light and a camera and reads back none of them has silently dropped part of the scene. This page's claim is that exactly one of each came back, and that the camera is still the kind it was written as.

if (_readBack.lights.length != 1) {
  throw StateError('the light did not survive the round trip');
}
if (_readBack.cameras.length != 1) {
  throw StateError('the camera did not survive the round trip');
}
final ModelCameraProjection projection =
    _readBack.cameras.single.projection;
if (projection is! ModelPerspectiveCamera) {
  throw StateError('the camera came back as the wrong projection');
}
if (frame.drawCalls < 1) {
  throw StateError('the cube was not drawn');
}