Skip to content

Stating a scene

Every frame, you hand the renderer a complete OrblitScene. Not a diff, and not a set of commands: the whole thing, built from scratch.

OrblitScene(
camera: ...,
objects: [...], // all of them, every frame
lights: [...],
materials: [...],
)

Flutter makes the same bargain about widgets, and it is worth understanding why it is not as expensive as it looks.

Building the scene object costs an allocation and a loop over your game state. For a few thousand objects that is microseconds, and it is work you were doing anyway to decide what to draw.

Sending it does not cost a re-upload. The objects are keyed, so object 7 this frame is object 7 from last frame. The renderer compares the two and sends only what changed. An object whose transform is identical to last frame’s costs nothing beyond the comparison.

So the expensive thing, talking to the GPU, is proportional to what changed. The cheap thing, describing the world, is proportional to how big the world is.

The scene cannot drift. There is no scene.add() you forgot to pair with a scene.remove(). An object that is not in this frame’s list is not in the scene, full stop. That whole category of bug, where the renderer’s idea of the world and the game’s idea of it slowly part company, does not exist here.

Time travel is free. If the scene is a function of your state, rewinding your state rewinds the picture. That is what makes the sequencer and the network’s interpolation possible without either of them knowing anything about the renderer.

Testing is possible. A scene is a value. You can build one in a unit test and assert on it, with no window, no GPU and no renderer. Most of orblit_filament’s 332 tests do exactly that.

A key is an int you choose. Two rules:

  • Stable across frames. The renderer identifies an object by its key. If the key changes, the old object is destroyed and a new one created, which is sometimes what you want and usually a bug.
  • Unique within the scene. Two objects sharing a key are one object, described twice, and you should not rely on which description wins.

The usual answer is to use your entity id, which already has both properties.

An OrblitObject is tracked on its own. It has a key, it is compared against last frame, it gets its own entity, and it gets its own draw call. That is the right trade for hundreds of things and the wrong one for hundreds of thousands.

The draw call has one exception. With batching, which is on by default, four or more placeholder cubes with the same material, colour and flags are drawn together, sixty-four to a draw, while each keeps its own key. Named meshes are not merged yet.

An OrblitPopulation is the other end of that. It is a buffer of transforms and colours, drawn instanced, sixty-four to a draw, with no keys, no per-item comparison and no per-item state. You hand over a Float32List and it gets drawn.

OrblitScene(
camera: ...,
objects: [player, ...props], // dozens, each individual
populations: [OrblitPopulation( // hundreds of thousands, in bulk
key: 1,
transforms: _transforms, // 16 floats each
colours: _colours, // 3 floats each
minimum: Vector3(-50, 0, -50), // the bounds they all sit inside,
maximum: Vector3(50, 4, 50), // so the lot can be culled at once
)],
)

Two hundred thousand members runs at about 27 ms a frame on an M-series Mac. The Benchmark example in the gallery lets you move the dials yourself. It reports what a frame actually costs the GPU, not a frame rate, and that is deliberate: how often a frame is presented is the display’s business, and it looks identical whether the engine has ten per cent of headroom or two hundred.

A scene can ask for something that cannot be given: a mesh file that will not load, or more lights than the view can shade. Those come back through onSceneNotes as a map, keyed by what was asked for and valued by what was wrong with it.

OrblitView(
scene: scene,
onSceneNotes: (notes) => setState(() => _problems = notes),
)

They come back, on purpose, instead of going into a log. A scene gets built out of things somebody typed a path to, and the person who typed it is the one who needs to know it was wrong.