Skip to content

From zero to a GPS anchored cube

S edited this page Aug 20, 2026 · 3 revisions

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.

What you install

pnpm add gps-plus-slam-app-framework three @zip.js/zip.js h3-js

The 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.

The three moving parts

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 instructive part

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.

Where content belongs in the scene

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 arWorldGroup at 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, inside arWorldGroup — 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.

What to expect on your first run

  • 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.

Where to go next

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.

Clone this wiki locally