Repository navigation
From zero to a GPS anchored cube
The smallest end-to-end example in this project does one thing: it starts a GPS-aligned AR session, tracks real surfaces under the screen centre with a reticle, and lets you tap to place an object that stays where you put it.
It is deliberately close to the stock three.js webxr_ar_hittest example, so
if you have read that, the shape is familiar and only the location-aware parts
are new.
pnpm add gps-plus-slam-app-framework three @zip.js/zip.js h3-jsThe framework brings the alignment core with it, along with Redux Toolkit and
gl-matrix. three is a peer dependency so your app owns the version.
Leaflet and Sentry are optional, needed only if you use the map overlay or
error reporting.
A store. The framework is composable around a Redux store you create:
import { createSlamAppStore } from 'gps-plus-slam-app-framework/state';
import { NullStorageBackend } from 'gps-plus-slam-app-framework/storage';
const store = createSlamAppStore({ storageBackend: new NullStorageBackend() });NullStorageBackend is the right choice while learning — it satisfies the
interface and persists nothing. Swap it later when you want recording.
A session. Entering AR needs a support check, three permissions, sensor watches, and the session itself. Rather than an opinionated button component, the framework exposes a headless controller with observable state, and your app renders whatever button it likes over it:
import { createEnableGpsArController } from 'gps-plus-slam-app-framework/ar';
const controller = createEnableGpsArController(/* … */);This matters more than it looks: it means the framework never owns your UI. It orchestrates; you render.
A reticle. Surface detection uses the WebXR hit-test feature, which the
session must opt into (features.requestHitTest). startHitTestReticle is
the turnkey driver — it gives you a reticle that tracks the surface under the
screen centre, which is where the user expects to place things.
The minimal example places two objects on tap, and the difference between them is the entire point of the project:
- One is parented to the scene root, at world coordinates — the "floater". Its world pose is exactly right and never changes.
- The other is created through
createGpsAnchor, which places it inside the AR tracking frame and re-derives its local position from its GPS coordinate each tick.
The naming is a deliberate joke at the naive intuition's expense. The floater is the one that is correct: its latitude and longitude never move. It looks wrong anyway, because when the alignment is corrected the camera shifts inside the AR group while the floater stands still, and a stationary object seen from a moving camera is indistinguishable from a moving object.
The anchor rides the group with the camera, so it holds still on screen, while its position is continuously re-derived so it also holds still on Earth.
Place both, then walk twenty metres and look back. Early in a session, before the alignment has evidence, they sit close together. As you walk, the gap opens — and watching it open teaches the distinction between "correct in world coordinates" and "looks correct on screen" faster than any explanation.
There is one convention to internalise, and it is the reverse of the usual
arrangement: the scene root is world space, and the AR tracking frame
(arWorldGroup) is a group inside it carrying the alignment matrix. The
camera lives in that group, so the camera moves when alignment is corrected;
objects parented to the scene root do not.
Three placements follow, and the middle one is the one people miss:
- Scene root, at world coordinates — permanently correct, visually floats.
-
Inside
arWorldGroupat a fixed local transform — welded to the tracked world, so it stays exactly where the user put it for the session, at the cost of its GPS position drifting. This is where user-created content belongs: a trail of the path they walked, a marker they placed by hand. -
createGpsAnchor, insidearWorldGroup— visually stable and correct, because the local position is re-derived from the coordinate every tick. The factory throws if the object is not a descendant of the group.
Not everything in a location-based app wants a location.
- Android, Chrome, ARCore. iOS does not expose WebXR AR — see capability gating for what to show the half of your visitors who cannot run it.
- HTTPS or localhost. WebXR and geolocation both require a secure context; for phone testing, either deploy or use a tunnel.
- Outdoors, and walk. Standing still in a doorway is the worst case for both GPS and the alignment estimator.
- Walk before judging stability, and give it more than a few seconds. The estimator has no evidence until you have moved: standing still tells it about receiver noise, walking tells it about geometry. How long convergence actually takes is a number worth being careful about — the project's own corpus measurements are less flattering than the rule of thumb, and are discussed in the piece on measuring alignment.
Once the cube stays put, the interesting questions are which object type each piece of your content should be — session-local or GPS-anchored — and how to make placements survive a reload and a share.
The persistent-anchor starter is the next rung: the same idea plus URL-based persistence, so a placement can be opened on someone else's phone and land in the same physical spot. It runs in a browser, so you can try that before writing any code. The example this article describes is deployed too, and both live in the repository.
Start here
Building with it
How the alignment works
Testing & debugging
Perception
Shipping it
12 more pages — see the page list above.