Files

7.7 KiB
Raw Permalink Blame History

Pages

Every HTML page the exhibit serves, and what it's for.

Production base URL: https://newburynights.hideawaygaming.com.au/

Keep this current. When a page is added, renamed, or removed, update this file in the same commit — it's the index there is, since there's no /admin/ landing page (admin URLs must be typed in full).

Visitor

Path Page What it does
/ Exhibit Landing screen → camera AR view. The thing visitors use. Tracking HUD appears in dev mode.

Admin

Open in EXHIBIT_MODE=dev; password-protected in EXHIBIT_MODE=final (ADMIN_PASSWORD in .env).

Path Page What it does
/admin/layout.html Layout Editor (3D) Marker anchors, building occluders, spawn points, wander paths, table size. Save/load named layouts.
/admin/playlist.html Playlist Tracks (set, spawn timing, concurrency, priority), residents, behaviour tuning, time windows.
/admin/characters.html Characters Per-ghost visuals: model, part placement, gradient colours, opacity, height, scale, face/torso textures.
/admin/landing.html Landing Page Visitor landing screen: title, logo, background, accent colour, about text, disclaimer.
/admin/preview.html Virtual Viewport Live orbit view of whatever ghosts are currently active, straight off the same WebSocket feed as the exhibit. Ground truth when the AR view disagrees.
/admin/testsheet.html A3 Test Rig Two printable A3 pages — calibration sheet plus a foldable marker tower — and a one-click load of the matching scene + playlist. See below.
/admin/plan.html Placement Plan Measured plan for physically positioning crests and models on the real table.
/admin/print.html Print Crests Generates printable ArUco markers ("Newbury Crests") at the configured sizeMM.
/admin/calibrate.html Calibrate Camera/marker calibration helper. Detects crests without filtering against the scene, so it's the quickest way to prove a printed crest is readable at all.
/admin/errors.html Client Errors Errors reported by visitor devices (JS errors, unhandled rejections, ghost build and scene-fetch failures). First stop when something works on one phone but not another.
/admin/login.html Login Only relevant in final mode.

The A3 test rig

/admin/testsheet.html is a self-contained test environment for evaluating tracking, occlusion and lighting without the LEGO town in the way. Two pages, both A3 landscape.

Print at 100% ("Actual size"), never "Fit to page." Check the 100 mm scale bar on page 1 first: if it doesn't measure 100 mm, every crest is the wrong size and every pose is scaled with it.

Page 1 — the ground sheet. The sheet is the table: A3 landscape is exactly 42.0 × 29.7 cm, origin at the front-left corner, matching the exhibit's world frame. Six 60 mm crests (ids 05) spread 38.7 cm corner to corner. Marked on it: the tower footprint at centre, the walker's loop, and the static ghost's spot.

Page 2 — the tower. Folds into an 85 mm square × 160 mm tall prism with a 60 mm crest on each of its four sides (ids 69, centred 100 mm up) plus one on top (id 10). Fold printed side OUT, tape the glue tab, fold the top flap over, and stand it on the dashed square with the FRONT panel toward the front edge.

Why the tower exists, and why it isn't optional: crests lying flat all share one plane, and a coplanar point set has two poses that reproject almost equally well — the true one and a mirrored twin. With few crests in frame the solver intermittently returns the twin, which looks like ghosts snapping to the wrong side of the table and vanishing. Reprojection error can't tell them apart (the twin fits the pixels). A crest 100 mm above the sheet makes the geometry properly 3D, and no mirrored pose can fit it. cv-worker.js also gates poses on physics (camera must be above the sheet, can't teleport) but that only suppresses the symptom — the tower is the cure.

The tower doubles as the occluder. The static ghost sits at (21, 22) cm, deliberately behind it from the front: walk around and the ghost should disappear behind the tower. The walking ghost runs an 18 × 12 cm loop around it, 60 cm perimeter, ~40 s per lap at the default 1.5 cm/s.

"Load this layout onto the server" writes both the scene and the playlist, built from the same constants that drew the sheets, so print and config can never drift apart. It replaces the live scene and playlist — save your current layout first.

Known gap: the layout editor doesn't understand cylinder occluders or wall-mounted anchors well, so opening and saving this layout there may mangle the tower.

Useful API endpoints

Read-only ones are handy to open directly in a phone browser while debugging; the rest need auth in final mode.

Endpoint Returns
/api/info Mode (dev/final) and build info
/api/scene Live scene: anchors, spawns, paths, buildings, table
/api/playlist Live playlist config (tracks, residents, behaviours)
/api/ghosts Full ghost roster + colour gradients
/api/characters Per-ghost visual overrides
/api/models Uploaded OBJ model manifest
/api/landing Landing page content
/api/layouts Saved named layouts
/api/client-errors Reported client errors (auth in final mode)
/ws WebSocket: scene, active, spawn, despawn, characters, pong

Reading the dev HUD

ws open  rx active,pong          WebSocket state + recent message types
scene 11 anchors  ghost recs 2   scene loaded, ghosts held client-side
60 fps  detect 20Hz@960  fov 58  render rate, detect rate/resolution, camera fov
  REJ:jump                       (only when the solver refused a pose)
eng opencv  moving               engine; FROZEN = ghost motion paused
axis std  cv:ready               engine mode + OpenCV worker state
markers 3/4 seen [0,4,6] +1dup   solved/raw crests, ids, duplicate decodes
raw / view / up / pos            solved camera pose
m0 px(0.02,-0.05) proj(0.03,-0.04)   AIM CHECK — see below
ghost ndc(-0.10,0.20) ON-SCREEN  first ghost's screen position

Aim check. px is where the crest actually sits on screen (0,0 = centre); proj is where the solved pose projects it. Point straight at a crest so px is near (0,0): if proj matches, the camera model is right. A sign flip means a mirrored axis; a constant ratio means the projection doesn't match the lens (tune localStorage.nbx.hfovDeg); an intermittent horizontal flip with correct vertical is the coplanar ambiguity — stand the tower up.

Gotchas worth remembering

Detection filters against the active scene. A crest that isn't an anchor in the current layout is detected and then discarded, which used to look exactly like "the camera can't see anything". The scene is fetched over HTTP at start so a flapping WebSocket can't break tracking, and the HUD distinguishes the cases.

The video stream may be portrait. iOS often hands back 720×1280, so any FOV assumption must apply to the image's LONG axis, not videoWidth. Getting that wrong made the focal length 1.78× too short.

Motion amounts live on the server. playlist.js sends explicit bobAmp, radius, speed values with every spawn, so the client's defaults never apply to a real ghost. Tune motion in the playlist config (behaviors.bobAmp, wanderRadius, pathSpeed), not in behavior.js.

An upstream proxy can override cache headers. The app sends no-cache for JS and HTML, but openresty in front has been seen replacing it with a daily expires. If a deploy doesn't reach a device, check the response headers and test in a private tab.