174 lines
8.5 KiB
Markdown
174 lines
8.5 KiB
Markdown
# Newbury Exhibit
|
||
|
||
Shared, view-only AR experience for a **physical Newbury LEGO display** (max 700 × 1500 mm).
|
||
Multiple phones/tablets see the *same* ghosts, in the *same* real-world spots on the build,
|
||
each from their own viewpoint — the "Hidden Side" made visible to a room full of people at once.
|
||
|
||
This is a sibling project to **newbury-nights** (the single-player hunt). The architecture is
|
||
deliberately different: instead of ghosts spawning around one player, ghosts live at fixed
|
||
**world anchors** on the build, and every device localises itself against printed/LEGO markers
|
||
so they all agree on where those anchors are.
|
||
|
||
---
|
||
|
||
## How it fits together
|
||
|
||
```
|
||
physical marker ──► world origin ──► anchors ──► ghosts ──► broadcast ──► every phone
|
||
(Newbury Crest) (one corner) (scene.json) (motion) (WebSocket) (renders identically)
|
||
```
|
||
|
||
1. **Localisation.** Each device's camera detects an ArUco 4×4 marker of known size and known
|
||
world position. From the marker's four corners it solves where the device is in the build's
|
||
coordinate frame. (Proof of concept: `public/ar-test.html`.)
|
||
2. **Authoritative state.** `server/sync-server.js` runs one motion loop and broadcasts ghost
|
||
poses at 20 Hz over WebSocket (`/sync`). The server is the single source of truth, so all
|
||
viewers see synchronised motion.
|
||
3. **Rendering.** Each device renders its own scene locally. `public/js/viewer.js` is the desktop
|
||
"god-view" for authoring/testing; the phone AR client (next milestone) will render ghosts at
|
||
anchor world-positions seen through the camera.
|
||
|
||
All coordinates are in **millimetres**, world origin at the **front-left corner** of the baseplate.
|
||
This maps 1:1 to the physical build, so tape-measure values drop straight into `data/scene.json`.
|
||
|
||
---
|
||
|
||
## Markers — "Newbury Crests"
|
||
|
||
The markers are **ArUco 4×4** (dictionary `ARUCO_4X4_1000`), chosen because a 4×4 data grid maps
|
||
cleanly onto LEGO tiles. Physically each marker is a **6×6 grid**: a 1-cell black quiet border
|
||
around the 4×4 code.
|
||
|
||
**Design decision — colour wheel as frame, not code.** ArUco detection is brightness-based
|
||
(each cell reads black or white), which is exactly what makes it robust under bad lighting and
|
||
shallow angles. Encoding the Red/Yellow/Blue Hidden Side colours *into* the code cells would make
|
||
it pretty but fragile. So:
|
||
|
||
- **Detector core (6×6):** black & white flat tiles — this is what the camera reads.
|
||
- **Colour-wheel ring:** a decorative Red/Yellow/Blue border *around* the core, in the zone the
|
||
detector ignores. On-theme for humans, invisible to the maths.
|
||
|
||
See `markers/` for printable SVGs, per-anchor LEGO build maps, and `crest-01_buildplan.svg`
|
||
(a visual of core + colour ring). Print a marker now to test robustness before committing to a build.
|
||
|
||
### Marker → anchor mapping
|
||
`markers/manifest.json` maps each physical ArUco id to a scene anchor. The `fiducialId` in
|
||
`data/scene.json` must match the `arucoId` of the marker you place there.
|
||
|
||
| Crest | ArUco id | Ward (placeholder) |
|
||
|-------|----------|--------------------|
|
||
| crest-01 | 0 | North-West |
|
||
| crest-02 | 1 | North-East |
|
||
| crest-03 | 2 | Town Square |
|
||
| crest-04 | 3 | South-West |
|
||
| crest-05 | 4 | South-East |
|
||
|
||
---
|
||
|
||
## Proof of concept: `ar-test.html`
|
||
|
||
Camera → marker detection → a ghost sprite locked to the marker, bobbing above it. No server
|
||
needed for this page. Use it to test detection robustness on your iPhone 15 Pro.
|
||
|
||
**Must be served over HTTPS** for the camera to work on iOS. Behind your existing nginx that's
|
||
already handled; for the exhibit it'll sit at the same kind of URL as newbury-nights.
|
||
|
||
Drop an animated WebP at `public/assets/ghost_blue.webp` to use a real sprite; otherwise it draws
|
||
a procedural blue "Sad" wisp.
|
||
|
||
---
|
||
|
||
## Forgiving scanning (`js/detect-tuned.js`)
|
||
|
||
Raw js-aruco can feel finicky — slow to acquire, quick to drop on blur, jittery once locked.
|
||
`detect-tuned.js` wraps it with three tolerances, used by `ar-head.html`:
|
||
|
||
1. **Looser detector params** — smaller min marker size, looser polygon fit, adjustable light
|
||
threshold. Three presets: `strict` / `forgiving` (default) / `greedy`. Switch live from the
|
||
"scan:" dropdown in `ar-head.html`.
|
||
2. **Multi-scale pass** — also detects on a half-res copy, catching distant / motion-blurred
|
||
markers that fail at full res (corners scaled back up).
|
||
3. **Coast + smooth tracker** — `MarkerTracker` keeps the last good pose for ~280 ms through a
|
||
dropout (no more snapping off when a hand passes or the image blurs), and smooths corners with
|
||
an adaptive 1€ filter: heavy smoothing when you hold still (kills jitter), light when you move
|
||
fast (stays responsive).
|
||
|
||
Tuning knobs: `DETECT_PRESETS` (params per preset), `MarkerTracker({ coastMs, smooth })`.
|
||
Verified that the looser presets don't introduce false negatives — all presets still cleanly
|
||
detect a known marker, including small + blurred.
|
||
|
||
> The jitter meter (`jitter.html`) deliberately does **not** smooth — it reports raw pose truth so
|
||
> the numbers are honest. `ar-head.html` smooths because there the goal is a stable-feeling ghost.
|
||
|
||
## 3D LEGO-head ghost: `ar-head.html`
|
||
|
||
The real-3D successor to the flat-sprite PoC. Camera → ArUco → **full 6-DoF pose** → a Three.js
|
||
ghost shaped like a classic minifig head (cylinder body, domed top, the stud) in a translucent
|
||
Hidden Side gradient with a fresnel rim-glow. Because it's driven by the marker's solved
|
||
rotation+translation, the head holds its orientation in world space — **walk around the crest and
|
||
it turns**, showing its sides. Eyes on the front (+Z) face; simple original ghost eyes, no
|
||
reproduced LEGO face print. Tap the swatches to switch lure colour (Blue/Red/Yellow).
|
||
|
||
Geometry lives in `js/lego-head-ghost.js` (`buildLegoHeadGhost(THREE, colorKey)`), built from
|
||
primitives so there's no model file to load. Set `MARKER_SIZE_MM` in `js/ar-head.js` to match your
|
||
marker.
|
||
|
||
> Pose convention: POS-IT gives the marker pose in a camera frame (X right, Y up, Z toward viewer);
|
||
> we flip Y/Z into Three.js's (Y up, looking down −Z). Verified: anchor lands in front of camera at
|
||
> unit scale.
|
||
|
||
## Pose jitter meter: `jitter.html`
|
||
|
||
Measures how *stable* the solved camera pose is per marker while you hold still — the number
|
||
that predicts whether occlusion will look clean. "Jitter" = standard deviation of the marker's
|
||
solved position (mm) and orientation (deg) over a ~1s rolling window.
|
||
|
||
**Workflow for the en-masse test:**
|
||
1. Open `jitter.html` on the phone (HTTPS).
|
||
2. Label the run in the text box (e.g. `1 marker, 30cm, room light`).
|
||
3. Hold steady on a crest, **Record** ~10s, **Stop**.
|
||
4. Repeat for: closer / further, sharper angle, 1 marker vs several crests in frame, bright vs dim.
|
||
5. **Export → Download JSON** and send the file back.
|
||
|
||
The HUD shows live pos-jitter (green <0.5 mm, amber, red >1.5 mm), angle jitter, distance, and
|
||
view angle. `distMeanMm`, `markersPerFrame`, and per-marker `posJitMedMm` in the export are the
|
||
key numbers — they'll tell us how much multi-marker fusion is worth before we build occlusion.
|
||
|
||
> Set `MARKER_SIZE_MM` at the top of `public/js/jitter.js` to the outer black-square size of the
|
||
> marker you actually test with (default 96 mm = 6 cells × 16 mm). Distance readings depend on it.
|
||
|
||
## Run
|
||
|
||
```bash
|
||
npm install
|
||
npm start # PORT defaults to 34034
|
||
# open http://localhost:34034/ar-test.html (camera needs HTTPS on phones)
|
||
# open http://localhost:34034/ god-view viewer (viewer.js)
|
||
```
|
||
|
||
Env: `PORT` (default 34034), `TICK_HZ` (default 20).
|
||
|
||
---
|
||
|
||
## Status / roadmap
|
||
|
||
- [x] Authoritative sync server (hover + patrol motion solvers, 20 Hz broadcast)
|
||
- [x] God-view desktop viewer
|
||
- [x] Scene + anchor + spawn data model (mm, corner origin)
|
||
- [x] ArUco 4×4 marker system + LEGO build maps ("Newbury Crests")
|
||
- [x] AR PoC: camera + detection + ghost locked to marker (flat sprite)
|
||
- [x] 3D LEGO-head ghost holding world orientation (`ar-head.html`)
|
||
- [x] Pose jitter meter (`jitter.html`) with session logging + JSON export
|
||
- [ ] Multi-marker pose fusion (one combined camera pose from all visible crests)
|
||
- [ ] Occluder geometry: ghosts vanish behind premapped buildings (depth-only boxes)
|
||
- [ ] Phone AR client: render anchored ghosts through the camera using marker pose
|
||
- [ ] Calibration page: scan each crest, record its measured world position
|
||
- [ ] Per-ghost colour/sprite wiring from the enriched roster
|
||
- [ ] Multi-device soak test on the real build under exhibition lighting
|
||
|
||
---
|
||
|
||
## Licence
|
||
MIT (project). Vendored `js-aruco2` is MIT (Damiano Falcioni / Juan Mellado); ArUco dictionary
|
||
data is 3-clause BSD (OpenCV). See headers in `public/vendor/`.
|