Skip to content

Exporting and importing scenes

A scene file is Orblit’s own format and it keeps everything. Exporting is for everything else: sending a scene to Blender, shipping one file instead of a folder, handing geometry to a tool that only speaks OBJ.

import 'dart:typed_data';
import 'package:orblit_scene/orblit_scene.dart';
SceneWritten export(SceneDocument scene, MaterialLibrary materials) {
final written = scene.writeAs(
SceneFormat.glb,
name: 'old_town',
materials: materials,
files: const <String, Uint8List>{}, // the bytes of anything it points at
);
for (final problem in written.problems) {
print('not carried: $problem');
}
// written.files, in the order to save them: name and bytes each.
return written;
}

files are handed over as bytes rather than as a way to read them, deliberately: this package has no filesystem in it, and the same export runs in the editor, in a cook step and in a browser. Only the caller knows which.

problems is what the format couldn’t hold, in the order it was met. Empty is the ordinary case and means nothing was lost.

Keeps the tree Files For
SceneFormat.gltf yes name.gltf + name.bin version control, reading in an editor
SceneFormat.glb yes name.glb shipping
SceneFormat.obj no name.obj + name.mtl tools that read nothing else

files comes back in the order to save it in: the first names the export, the rest are the sidecars it points at. SceneFormat.keepsScene is the same distinction as a value, for a menu that needs to warn before OBJ.

OBJ is corners and faces in world space. Lights, cameras, the tree and every component that isn’t geometry are gone, and each is listed in problems on the way out. It’s there because everything opens it.

FBX is deliberately not a format you can write to. Reading one is worth the trouble because people have them, and Orblit does, through ufbx. Writing one is a proprietary format with no public specification, and everything that opens an FBX opens a glTF.

An entity whose mesh names an imported model — MeshComponent(asset: …) — is exported with that model’s glTF copied in whole, its roots becoming children of the entity’s node. Meshes, materials, textures, skins and animations all come with it. That’s what makes the export openable somewhere else rather than a tree of empty nodes with paths in them.

It needs the bytes, which is what files is for, keyed by the project path the scene names the model by. A model whose bytes weren’t supplied exports as an empty node where the model goes, and says so in problems.

The bytes have to be a GLB. Only a model carrying its own buffer can be copied into an export — a .gltf with a .bin beside it names something the exporter has no way to follow, and is refused with a problem rather than grafted half way.

A model placed twice is copied once. glTF already lets any number of nodes draw one mesh, so the second placement copies the nodes and nothing else — no bytes, no accessors — and a scene with two hundred of the same tree carries one tree. The exception is a model with a skin or an animation: sharing those would have both copies bent and moved by the first one’s bones, so they’re copied again in full.

import 'dart:typed_data';
import 'package:orblit_scene/orblit_scene.dart';
SceneDocument import(Uint8List bytes) {
final read = readSceneFrom(bytes, name: 'Old Town');
if (!read.wasWrittenHere) {
// Someone else's glTF. Still a scene, with rather more caveats.
}
for (final problem in read.problems) {
print(problem);
}
return read.document;
}

Whether the bytes are a GLB or glTF JSON comes from the bytes, not from the name. A .gltf that is really a GLB is an ordinary thing to be handed, and a reader that trusts the extension fails on it with a JSON error nobody can act on.

A .gltf points at its .bin by name. Pass those bytes in files, keyed by the URI the document names them by — and note that a scene called old town.gltf writes a buffer named old%20town.bin while the file on disk has the space in it. readSceneFrom looks under both.

SceneFormatException is thrown only when there’s nothing to read at all: bytes that are neither a GLB nor JSON. Everything short of that comes back in problems, because a scene with one unreadable node is still a scene worth opening.

The exporter writes a full record of itself. Every node carries an extras.orblit holding the entity’s id, whether it’s visible and each component as the component itself serialises it; the scene carries its settings the same way. So reading an Orblit export back is a read, not a re-derivation from matrices and materials, and the document that comes out encodes byte for byte to the one that went in.

import 'package:orblit_scene/orblit_scene.dart';
bool roundTrips(SceneDocument scene) {
final read = readSceneFrom(scene.writeAs(SceneFormat.glb).first.bytes);
return read.document.encode() == scene.encode();
}

That’s asserted in the test suite, not hoped for. Two consequences worth knowing:

  • A component this build has never heard of survives. It comes back as an UnknownComponent holding its JSON, so an older editor can open a scene a newer one saved, change something else and save it without quietly dropping what it didn’t understand.
  • Grafted models don’t become entities. The nodes copied in from an imported model carry no extras.orblit, and the importer skips them for exactly that reason. Without it every round trip would double the outliner.

wasWrittenHere is how you tell the two cases apart. A glTF from anywhere else is read as best it can be — nodes become entities, transforms and lights and cameras come across — and each thing that had to be guessed at or dropped is listed in problems.

  • The Blender half isn’t automated. That an Orblit export opens correctly in Blender is checked by hand. Nothing in CI opens Blender.
  • Export is API-only. There’s no menu item in the editor yet; writeAs is the whole interface.
  • A second buffer isn’t read. Nothing Orblit writes has one. A foreign glTF with several is read as far as its first, and the accessors that reach past it say so in problems.
  • Animation on an Orblit entity isn’t exported. A grafted model brings its own animation with it; an entity animated by the scene doesn’t, because there’s nowhere in the document for that yet.