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 reads 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;
}

You hand files over as bytes, not as a way to read them. That is on purpose: 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. An empty list 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, and 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 one is listed in problems on the way out. It is there because everything opens it.

You can’t write FBX, and that is deliberate. Reading one is worth the trouble because people have them, and Orblit does, through ufbx. Writing one means 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, and the model’s roots become children of the entity’s node. Meshes, materials, textures, skins and animations all come with it. That is what makes the export open properly somewhere else instead of being 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, so it 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. A model with a skin or an animation is the exception: sharing those would have both copies bent and moved by the first one’s bones, so they are 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 a normal 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. 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 is 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 is visible and each component as the component itself serialises it, and 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 is asserted in the test suite, not hoped for. Two consequences are 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 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 well as it can be — nodes become entities, and transforms, lights and cameras come across — and everything that had to be guessed at or dropped is listed in problems.

  • The Blender half isn’t automated. Somebody checks by hand that an Orblit export opens correctly in Blender. Nothing in CI opens Blender.
  • Export is API-only. There is 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 is nowhere in the document for that yet.