The Scene Document Model: Document Structure for 3D Elements
Most 3D engines give a scene a graph but not a document: things to draw, addressed by renderer handles, mutated imperatively — with no bubbling events, no stable authored addresses, and no save format that is the single source of truth. The Scene Document Model takes the opposite stance — it gives a pure 3D scene graph the structure of a document, so that solids, lights, and groups can be addressed, listened to, animated by name, saved, and reloaded the way the elements of a web page can. This page is the definition of the Scene Document Model, the model a 3D Spatial Web Interface is built on: 3D elements given document behavior.
What is the Scene Document Model?
Scene Document Model — an architecture in which the elements of a 3D scene — solids, lights, groups, cameras — carry the structure and behavior of a 2D document: a parent/child element tree, events that bubble along it, stable addresses, per-element state, and serialization that round-trips, so a 3D scene can be authored, queried, saved, and reasoned about the way a web page can.
None of these elements is HTML: every document behavior — the tree, the events, the addressing, the save format — is deliberately rebuilt over the engine's own records. The payoff is that decades of the 2D web's structural wisdom (nesting, bubbling, ids, components, hide-versus-remove) applies to geometry and light.
An element tree, not a display list
The model's substrate is a parent/child tree of addressable records, not a flat list of draw
calls. A group is a container in the same sense a <div> is: it parents its
children, its subtree moves and serializes as a unit, and a live group's subtree can be captured
whole into a reusable definition. A message bubbled from a record climbs its parent chain toward the
scene root, which is what makes the tree structural rather than cosmetic — hierarchy determines
who hears what, not just what moves with what.
The tree stays honest through a strict mechanism-versus-content rule. A renderer needs internal structures the author never made — the reference implementation, for example, re-parents top-level records into bounding-volume-hierarchy cull nodes to prune ray marching. Those nodes are mechanism, excluded by identity from the document: saved scenes never contain them, queries never report them, and picking never selects them; authored structure is restored whenever the document is read or written. Membership is a judgment the engine must enforce — the document is what the author made, never what the renderer built to go fast.
The DOM concept map
Each load-bearing concept of the 2D document model has a specific equivalent in the Scene Document Model. The table below is the map; the rows are elaborated after it.
| 2D document concept | Scene Document Model equivalent |
|---|---|
| Element | Scene record — a solid, light, group, or camera display, each with an id that stays fixed while the scene runs. |
| Parent/child nesting | Groups — container records whose subtree moves, captures, and serializes as a unit, and through which events bubble. |
id / human-readable class | Id / authored label — a label saves with the scene, and the reference implementation's lookups (its query interface and the host page's port) accept it in place of an object's id, though a state-machine binding matches only an exact id or '@self'; it need not be unique, and a duplicate produces a which-one question, never a guess. |
| CSS transition | Named animation clip playing from a 'current' sentinel frame that resolves from live state at play time — a glide from wherever the element is now. |
| Event listener | State-machine bus binding — a { channel, action, when, transition } subscription that fires a transition carrying the message payload, with '{{payload.key}}' templating to lift values into actions. |
| Reusable component | Composite — parameterized structure, serialized as its name-plus-params pair — and archetype: shape + clips + state machine, so the same shape can carry different semantics. |
| Document save | The scene document — reconstitutable definitions, kept deliberately separate from reported live state. |
display:none | Park — present but deliberately not participating: the record keeps its id, hierarchy position, and state, but is not drawn, not lit, and not clickable. Transient by ruling; never serialized. |
Three rows deserve emphasis. Clips make the model's mutation convention analog-first:
a state change is authored as a two-frame clip from 'current' to the target and
played, so every change is continuous motion rather than a teleport — the 3D equivalent of never
setting a property a CSS transition should have eased. The browser computes the easing (a Web
Animations API timeline interpolating registered CSS custom properties) and the engine writes the
sampled values into GPU records of every kind (see the animation
bridge).
Archetypes make state per-instance: every instance gets its own state machine sitting at its
initial state, unfired, so two instances are never in lockstep unless explicitly choreographed.
And park is precisely not a soft delete: it answers the same need
display:none answers in 2D — still in the document, temporarily out of the experience
— and because it is transient by design, a saved scene never records it and a loaded scene comes
back fully participating.
Events that bubble through 3D
The model's event system rebuilds DOM event semantics over the record tree — it does
not use DOM events, and no DOM object is involved in delivery. In the reference implementation, a
synchronous scene bus carries frozen envelopes of the shape { channel, action, payload,
senderId, targetId, seq, timeStamp }. A bubbling delivery walks parent pointers from the
target record toward the scene root with stopPropagation semantics and a cycle guard;
a bubble that reaches the root unstopped is offered only to an explicit whitelist of registered
root actions, never to open method dispatch. The inverse traversal exists too:
broadcastSubtree delivers root-down through a subtree. Delivery is fault-isolated
per handler — a subscriber that throws is converted into a message on a reserved error channel
rather than killing the loop, so one broken listener never silences the rest of the scene.
Inside the scene the browser has no element to hit-test, so picking is a CPU ray cast — exact
against JavaScript twins of the distance fields for a handful of basic shapes, and against a
conservative bounding box for the rest — and the result is published on the bus's ui channel to that channel's subscribers, like any
other published message, rather than bubbled (see event propagation in 3D).
Layout machines for 3D volumes
Rigs are the model's layout containers: arrangement machines that position slabs — 3D content-bearing volumes — the way flex and grid containers position boxes. A rig owns persistent cells; slabs occupy them. The cells outlive their occupants: a cell is minted once, holds one occupant, and remains when the occupant moves elsewhere, so occupancy is a first-class, queryable fact of the document rather than an emergent property of positions. Re-arrangement is document-shaped too — solve the target layout, then glide every element from its live pose, interruptibly.
A rig's elements are volumes CSS cannot see — yet CSS can still be the solver: the reference implementation lays out a measurement twin with the author's ordinary CSS grid — an offscreen container of empty stand-in boxes, each carrying the declared size of the element it stands for — measures the resulting boxes, and converts the measured rectangles into world-space poses (see how CSS grid becomes 3D layout). The twin is neither the platform's Shadow DOM nor a hidden state store — it exists only to be measured. State for 3D elements lives with the elements themselves: in the records, their clips, and their machines, whose definitions the scene document saves — never in any hidden DOM.
A document that saves
A scene document round-trips because it stores definitions, not live state. Serialization is
split on purpose into two views: the document carries the reconstitutable shapes — clip keyframes
with 'current' preserved as a keyword, state machines as the plain JSON they were
authored in, composites as their name-plus-params pair — while live state (clip progress, a
machine's current state, message history) is reported separately for reading and is never written
into the document. Reloading a scene therefore replays construction rather than restoring a
memory image, which is what makes the saved form portable, diffable, and safe to edit by hand.
The same discipline keeps documents small: derived geometry never serializes. Where a scene has expanded a definition into many records — glyphs laid out from a text node, a mesh expanded into its triangles — the document stores a single reference node naming what to rebuild instead of what was built: a twelve-letter word is twelve glyph records under one group in the scene and one node in the document. And consistent with definitions-not-state, transient conditions stay out entirely — a parked element is never saved as parked; a loaded scene wakes with everything present and participating. The scene document is the single serialized truth for every 3D element.
Reference implementation
The Software 4 Senses 3D Spatial Web Studio implements the Scene Document Model as the stateful tier of its WebGPU engine. Every scene element — primitive, group, light, camera display — is an addressable record that can carry named clips and a declarative JSON state machine; a synchronous scene bus provides targeted delivery, parent-pointer bubbling with a root-action whitelist, and subtree broadcast; rigs place slabs through a CSS-measured twin; and scene documents serialize definitions — clips, machines, composites, reference nodes — separately from the live state the engine reports at runtime. The pages linked below cover the individual mechanisms in depth.
Related reading
- What Is a 3D Spatial Web Interface? — the architecture category, built on this model.
- Event Propagation in 3D: From Screen Space to Scene Records — how a click becomes an envelope on the record tree, in depth.
- Synchronizing Web Animations API Easing with a GPU Render Loop — browser easing driving engine records.