docs: document the tower and why coplanar crests flip the pose; add HUD reading guide
This commit is contained in:
+65
-28
@@ -26,7 +26,7 @@ Open in `EXHIBIT_MODE=dev`; password-protected in `EXHIBIT_MODE=final`
|
|||||||
| `/admin/characters.html` | Characters | Per-ghost visuals: model, part placement, gradient colours, opacity, height, scale, face/torso textures. |
|
| `/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/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/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 | Printable A3 calibration/evaluation sheet, and a one-click load of the matching scene + playlist. See below. |
|
| `/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/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/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/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. |
|
||||||
@@ -36,28 +36,45 @@ Open in `EXHIBIT_MODE=dev`; password-protected in `EXHIBIT_MODE=final`
|
|||||||
## The A3 test rig
|
## The A3 test rig
|
||||||
|
|
||||||
`/admin/testsheet.html` is a self-contained test environment for evaluating
|
`/admin/testsheet.html` is a self-contained test environment for evaluating
|
||||||
tracking, occlusion and lighting without the LEGO town in the way.
|
tracking, occlusion and lighting without the LEGO town in the way. Two pages,
|
||||||
|
both A3 landscape.
|
||||||
|
|
||||||
- **Print A3 landscape at 100% ("Actual size"), never "Fit to page."** Check the
|
**Print at 100% ("Actual size"), never "Fit to page."** Check the 100 mm scale
|
||||||
100 mm scale bar first: if it doesn't measure 100 mm, every crest is the wrong
|
bar on page 1 first: if it doesn't measure 100 mm, every crest is the wrong size
|
||||||
size and every pose is scaled with it.
|
and every pose is scaled with it.
|
||||||
- The sheet *is* the table — A3 landscape is exactly 42.0 × 29.7 cm. Origin is
|
|
||||||
the front-left corner, matching the exhibit's world frame.
|
|
||||||
- **6 crests** at 60 mm, spread 38.7 cm corner to corner (the spread is what
|
|
||||||
makes the joint solve well-conditioned).
|
|
||||||
- **Cylinder occluder**: stand an 85 mm ⌀ × 160 mm tall object on the dashed
|
|
||||||
circle at centre.
|
|
||||||
- **Static ghost** at (21, 22) cm, deliberately *behind* the cylinder from the
|
|
||||||
front — walk around it and the ghost should disappear behind the post.
|
|
||||||
- **Walking ghost** on an 18 × 12 cm rectangular loop around the cylinder;
|
|
||||||
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 sheet, 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, so opening
|
**Page 1 — the ground sheet.** The sheet *is* the table: A3 landscape is exactly
|
||||||
and saving this layout there will drop the cylinder.
|
42.0 × 29.7 cm, origin at the front-left corner, matching the exhibit's world
|
||||||
|
frame. Six 60 mm crests (ids 0–5) 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 6–9, 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
|
## Useful API endpoints
|
||||||
|
|
||||||
@@ -77,18 +94,38 @@ the rest need auth in final mode.
|
|||||||
| `/api/client-errors` | Reported client errors (auth in final mode) |
|
| `/api/client-errors` | Reported client errors (auth in final mode) |
|
||||||
| `/ws` | WebSocket: `scene`, `active`, `spawn`, `despawn`, `characters`, `pong` |
|
| `/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
|
## Gotchas worth remembering
|
||||||
|
|
||||||
**Detection filters against the active scene.** A crest that isn't an anchor in
|
**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
|
the current layout is detected and then discarded, which used to look exactly
|
||||||
like "the camera can't see anything".
|
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 scene is fetched over HTTP at start, so a down or flapping WebSocket no
|
**The video stream may be portrait.** iOS often hands back 720×1280, so any FOV
|
||||||
longer breaks tracking.
|
assumption must apply to the image's LONG axis, not `videoWidth`. Getting that
|
||||||
- The HUD reports `scene N anchors` (or `NOT LOADED`) and `markers 1/2 seen
|
wrong made the focal length 1.78× too short.
|
||||||
[0,7]` — usable over raw — so a filtered-out crest is visible at a glance.
|
|
||||||
- The tracking chip says "Crest N not in this layout" rather than "Point at a
|
|
||||||
Newbury Crest" when this happens.
|
|
||||||
|
|
||||||
**Motion amounts live on the server.** `playlist.js` sends explicit `bobAmp`,
|
**Motion amounts live on the server.** `playlist.js` sends explicit `bobAmp`,
|
||||||
`radius`, `speed` values with every spawn, so the client's defaults never apply
|
`radius`, `speed` values with every spawn, so the client's defaults never apply
|
||||||
|
|||||||
Reference in New Issue
Block a user