Stating a scene
Every frame, you hand the renderer a complete OrblitScene. Not a diff, not a
set of commands: the whole thing, built from scratch.
OrblitScene( camera: ..., objects: [...], // all of them, every frame lights: [...], materials: [...],)This is the same bargain Flutter makes about widgets, and it’s worth understanding why it isn’t as expensive as it looks.
What it costs
Section titled “What it costs”Building the scene object costs an allocation and a loop over your game state. For a few thousand objects that’s microseconds, and it’s work you were doing anyway to decide what to draw.
Sending it doesn’t 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, and an object whose transform is identical to last
frame’s costs nothing beyond the comparison.
Which leaves the expensive thing, talking to the GPU, proportional to what actually changed, and the cheap thing, describing the world, proportional to how big the world is.
What it buys
Section titled “What it buys”The scene can’t drift. There is no scene.add() you forgot to pair with a
scene.remove(). An object that isn’t in this frame’s list isn’t 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, doesn’t exist here.
Time travel is free. If the scene is a function of your state, rewinding your state rewinds the picture. That’s 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 hundred-odd 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 occasionally what you want and usually a bug.
- Unique within the scene. Two objects sharing a key are one object, described twice, and which description wins isn’t something to rely on.
The usual answer is to use your entity id, which already has both properties.
Populations are the other half
Section titled “Populations are the other half”An OrblitObject is tracked individually: it has a key, it’s compared against
last frame, it gets its own entity, and it gets its own draw call. That’s the
right trade for hundreds of things and the wrong one for hundreds of
thousands.
An OrblitPopulation is the other end of that. 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, and
reports what a frame actually costs the GPU rather than a frame rate. That’s
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.
The renderer answers back
Section titled “The renderer answers back”A scene can ask for something that cannot be given: a mesh file that will not
load, 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),)Deliberately back, rather than 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.
