Repository navigation
The scene graph convention
There is one structural fact in this framework that, once understood, answers a surprising number of "why is my object behaving like that" questions — and it is the opposite of what most people assume on first reading.
The scene root is world space. The AR tracking frame is a group inside it.
scene ← GPS-aligned (NUE) metres — the WORLD frame
├── arWorldGroup ← carries the GPS→AR alignment matrix
│ ├── camera ← the WebXR viewer pose
│ └── ar-content ← anything fixed in AR space
└── ..objects at GPS coords.. ← waypoints, POIs, navigation arrows
Most AR frameworks put the tracking frame at the root and hang world content off a child. This is the other way round, and the inversion is deliberate: the thing that is known — a latitude and longitude converted to metres — sits at the root, and the thing that is estimated — where the AR session's tracking frame sits in the world — is the transform on the group.
When the solver produces a new alignment, the framework writes it to
arWorldGroup.matrix, smoothly interpolated. The camera lives inside that
group, so the camera moves. Objects parented directly to the scene do not.
Ask the obvious question: where do I put a waypoint that must stay at its GPS coordinate?
The obvious answer is "the scene root, at its NUE coordinates", and it is
half right. The object's world pose is then correct forever — it never moves,
because the number never changes. But every time the alignment is corrected the
camera shifts inside arWorldGroup, and from the user's point of view a
stationary object with a moving camera is indistinguishable from a moving
object. The waypoint appears to float.
That is the trap. Being correct in world coordinates and looking correct on screen are different properties, and the naive parenting gets you the first at the cost of the second.
So there are three placements, not two:
- Under the scene root, at NUE coordinates. World pose exact and permanent; visually floats during corrections. Cheap and right for small markers where a little swimming does not matter — reference-point spheres, bulk map pins.
-
Inside
arWorldGroup, at a fixed local transform. Welded to the AR-tracked world, so it stays visually nailed to the spot for the whole session. Its world/GPS pose drifts as alignment is corrected, so it cannot be replayed or shared by coordinate. This is the right home for user-created local content: a trail of the path they walked, a hit-test reticle, a decoration they placed by hand on a particular bench. -
createGpsAnchor, insidearWorldGroup. The best of both, and the reason the helper exists: the object rides the group with the camera, so it is visually stable, and its local position is re-derived every tick from its GPS target and the current alignment, so it stays at its real coordinate. The factory throws if you hand it an object that is not a descendant of the group — placing an anchor under the scene root defeats the stability it exists to provide.
-
"My hand-placed decoration keeps sliding off the bench." It is a GPS
anchor when it should be a fixed local object inside
arWorldGroup. The user aimed it; corrections are moving it away from where they aimed. - "My waypoint looks like it is swimming as I walk." It is parented to the scene root, so the camera is moving under corrections while it stands still. Make it a GPS anchor.
-
"My waypoint was fine, then wrong after a long walk." It is inside
arWorldGroupat a fixed local transform, so it has been drifting with the tracking frame all along. - "Everything is in the right place but the whole scene is rotated." The alignment's heading estimate has not converged. Walk a few metres.
The framework could have offered addWorldObject() and addLocalObject() and
hidden the graph. It exposes the graph instead, because you are writing a
three.js app: you already know how to parent, group, traverse, raycast and
dispose, and wrapping that in a bespoke object API would mean re-exposing all
of three.js through a narrower door.
One caveat worth knowing: the exported SCENE_NODE constants cover the
framework's own internal nodes, not every node named in this article. Reach the
live graph through getScene(), getArWorldGroup(), getCamera() and
getCurrentArPose().
Ask what the object's truth is, and then ask whether it must also look fixed:
- A coordinate on Earth, and it must look nailed down →
createGpsAnchor, and see how it corrects itself. - A coordinate on Earth, and a little swimming is acceptable → scene root.
- A position the user chose in this session → fixed transform inside
arWorldGroup. - A position on the screen → the camera.
Start here
Building with it
How the alignment works
Testing & debugging
Perception
Shipping it
12 more pages — see the page list above.