# Open work on Tera Short, and only things that are decided but not done. Anything speculative belongs in an issue, not here. ## One command re-shoots the imagery: `npm run refresh` ```sh npm run refresh # build, stills, cards, films, both manifests npm run refresh -- --dry-run # what it would shoot; no browser, no build npm run refresh -- --stills-only # …--cards-only, --films-only; they compose npm run refresh -- --rough # films at 24 frames, for looking at npm run refresh -- --skip-build # you just built; do not build again ``` `scripts/brand-assets/refresh.mjs` exists because re-shooting used to be a *procedure* — build first, because the scripts photograph `dist/` and not `src/`; then three commands in an order that matters; then check the run actually got the GPU. A procedure that lives in somebody's head gets half-run, and the evidence was in this file: the stills were re-shot at one commit and the films at another two weeks older, and nothing said so until a human compared two files. It prints a diff of the deliverables at the end — new, changed with the size delta, byte-identical — and it **fails loudly and specifically** on the three failures that otherwise produce confident wrong output: - **SwiftShader.** Software rendering makes the same pictures fifteen times slower and reports success. Any child that prints a software renderer stops the run. - **A chapter `expect` guard firing.** That means a pack was reordered under the shot list, so re-running cannot fix it. `node scripts/brand-assets/shots.mjs --list` prints what the shot list believes. - **A `play` guard firing.** A shot that takes a body did not get the one it asked for — see the section below. Also not a flake, and also not fixed by re-running. Two things it does **not** do, on purpose: it does not commit, and it does not re-stamp the manifests afterwards. Commit tera first, then `shots.mjs --manifest-only` and `films.mjs --manifest-only`, so the manifests name a clean sha rather than a dirty tree. ## A shot can take a body now — `play` in `shots.mjs` Three stills are pictures of an agent rather than of a place: `drive-101` (chase camera behind the EV on US-101), `crow-nav` (the crow over the Bay Area board) and `california-flight` (the aircraft on the corridor). They replace three hand-made screenshots in `lumbridge-v4/apps/web/public/arena/`, which were the last imagery on the site no script could reproduce — and so also the last that nobody could re-shoot when the engine moved. All three still showed a left column reading "Explore as your crow →", a "FULL VIEW" badge no visitor to the site ever sees, and one daylight frame served to dark-mode readers as well. `play` drives the mode dock, which is the same class of input `aim` is: no back door, just the control a reader uses. `#mode-dock` and `#play-hud` stay **in** these three frames deliberately — they are what the picture is of — while remaining in `FURNITURE.BARE` for the share cards. Three guards, all of them tested by making them fire, because every way this goes wrong renders a plausible-looking frame with the right filename: - the dock button must be *offered*, not merely present — the EV and the aircraft exist on the California board alone, and driving also needs US-101 or I-5 to be the current chapter, so elsewhere the button is `hidden` and clicking it is a no-op that leaves an orbit wide shot; - `aria-pressed` **and** `#play-hud`, exactly as `performance-budget.mjs` does — `transitionControlMode` substitutes a fallback for a request it will not honour; - the HUD's own chip names the body ("Drive", "Crow", "Flight"), checked at the click *and again at the shutter*, because a route that completes or a bound that is hit ends the mode mid-wait. `refresh.mjs` surfaces all three as their own headline, next to SwiftShader and the chapter guard. **`untilProgress`, not a duration, for anything that was already moving.** The crow and the aircraft are built inert, so *n* ms after the dock hands them over is the same place every time — measured identical at 1x and 2x. Road traffic is not: it runs from page load, so a duration is really "boot plus a bit", and the same `run: 9000` put the EV at 55% of the leg in a 1x preview and 69% in the 2x render, which is a different stretch of road with the edge of a town across the top of the frame. `untilProgress` waits on the app's own route percentage instead, so preview and delivery agree. Overshoot throws rather than shooting. ### ~~`drive-101`'s night frame is the weak one~~ — fixed in the engine, 2026-08-22 This section used to say the frame's subject nearly disappeared, that the hour was not the lever, and that both candidate fixes were in the engine: *give the EV a headlamp spill on the road ahead*, or let the corridor's terrain keep some moonlight. **The first one landed.** The EV now throws a low beam on the road in front of it, the corridor carries continuous edge lines, a yellow median pair and cat's eyes, and the frame was re-shot at the same camera and the same hour. Canvas-region mean luminance, 1600w, before → after: **7.8 → 10.2** overall, and **5.5 → 11.7** on a 580×470 crop around the car and the road ahead of it. It reads as a car with its lights on rather than as two tail lamps. **What is still true, and is a capture limit rather than a light one.** The EV is about 55 px wide in a 1440 px frame and always will be from this camera: `applyFollow` in `roadTraffic.ts` puts the chase camera 0.72 back and 0.92 up with its target 0.32 ahead, which is a fixed 38° looking-down pose, so the road fills the picture and there is no horizon in it. The stretch of road is the only lever the shot list has, and it was swept: `untilProgress` 0.64 puts the car on a bend but loses the far carriageway, 0.72 lands it at an off-ramp with a settlement's buildings — drawn at 1,919 m to the unit — across the right of the frame, and 0.82 is a wider carriageway with the car smaller again. **0.55 is still the best of them** and stays. If the car is ever wanted larger, that is a change to the chase offsets in `roadTraffic.ts`, not to `shots.mjs`. The crow moved to the Bay Area board for the same reason and it worked: over a city the night frame is the *better* of its pair. The hand-made original was on the state board, where a one-metre bird stands in front of ground drawn at 1,919 m to the unit with an oak the size of a hill behind it. ### ~~`bay-bridge` was aimed at the wrong thing~~ — re-aimed, 2026-08-22 The old frame ran the crossing diagonally out of the top-right corner, left the entire right half of the picture as open water, and contained **only one of the two shores the bridge joins** — so its own `note` had to say so, and its caption described a wide bay view rather than the bridge the id promises. It now seeks to 37.8085, −122.363 (the causeway just east of Yerba Buena, which is the midpoint of *what has to fit* rather than of the structure, because the panel eats the left 310 px) at `zoom: -4`, and drags `azimuth: -45` to turn the chapter's almost-due-north stance broadside to a crossing that runs ENE at about 57°. `elevation: -6` drops the camera far enough to put Marin, Angel Island and the Golden Gate along the top, which is what stops the water reading as empty. Both landings are inside the frame, with San Francisco's skyline at one end and the East Bay shore at the other, and the whole 2013 topology — two suspension towers, the deck coming down onto the island, the third tower, the piered causeway — is legible across the middle. Caption, both `note`s and both `alt`s were rewritten to the frame that is actually delivered. Night, 1600w: canvas-region mean **8.3 → 16.7**, and on a 420×110 strip lying on the causeway — the half of the crossing the old frame did not contain at all — peak **18.7 → 134.9**. The night frame no longer needs a lift before anything appears, which is what this file used to record about it. ### ~~The Golden Gate needs an earlier hour than everything else~~ — retired `golden-gate`'s night frame was shot at 20:50 (sun −7.5°, nautical twilight) with a `note` explaining the exception: the other night frames are cities carried by their own windows, and a bridge over open water had none, so 21:35 rendered this one as a black rectangle. **The deck now carries its own lamps**, so it is back on the standard 21:35 with everything else and the special case is gone from `shots.mjs`. Worth knowing before anyone "fixes" the number: the canvas-region mean went **8.2 → 7.0** at the deeper hour, because losing the residual sky costs more mean luminance than 1,600 lamps over open water add back. **Measure the subject, not the canvas, on a bridge frame.** On the deck the lamp run, both tower silhouettes and the red light over each tower head all read clearly at 21:35, where at 20:50 there was no light on the structure anywhere. ## Re-shoot the time-lapse films All 38 product stills were re-shot on 2026-08-22 against the night-infrastructure work, so the manifest records `SHOTS_COMMIT b7f5c41` with **`SHOTS_DIRTY true`** — that tree was not committed when the shutter opened, and the manifest says so rather than naming a commit that would render something else. Re-run `shots.mjs --manifest-only` once it is committed. The two **share cards** were not re-shot and did not need to be: `capture.mjs` shoots both at 11:20 and 17:30, so nothing that changed after dark is in them. **The films were not** — they were last shot on 2026-08-07 at `9c9e78f`. `films.ts` in lumbridge-v4 records the tera commit each reel was shot at, so a stale reel is visually stale and no caption edit fixes it — check that commit against tera HEAD before assuming a reel is current. What the reels predate: ACES tone mapping and the runtime PMREM environment rig; a sea that reflects, with a swell normal map, Fresnel and sun glitter, in place of a flat `MeshLambertMaterial` that had no specular term at all; a world-space sky dome replacing the screen-space gradient, which at dusk painted the sunset band along the *bottom* of the picture, under the board; terrain that casts shadows; a California board with the Basin and Range, Death Valley, the Salton Sea and San Diego on it; SFO, LAX, both bridges and a freeway that reads as a road; aircraft that move, carrying registration and type; a rebuilt LA studio; the studio device hardware; and a new UI chrome with a first-run flow. ```sh npm run refresh -- --films-only --rough # rough cut first: ~20 s a reel, and it is # the cheapest way to find out a camera is # pointed at the wrong thing npm run refresh -- --films-only # ~90 s per 180-frame film on the Radeon ``` Worth knowing before starting (the rest is in `~/.claude/skills/tera-capture`): - Check the run prints the Radeon and not SwiftShader. The software path is ~15x slower — 22 minutes a film rather than 90 seconds — and it reports success. - `hero-soma` is a different animal: `chrome: "bare"`, midnight to midnight, and a two-stage encode (a CRF 18 master, then 1440w CRF 28 and 960w CRF 30 delivered). - **The hero camera is worth reconsidering.** Whole-board was tried and rejected because the edge of the terrain plate showed against the sky. That seam no longer exists: the sea now runs eighteen board spans and ends past the fog's far plane. - After tuning the hero, run `npm run check:hero` in lumbridge-v4. - `#onboarding-host` is in `FURNITURE.CLUTTER` now, and `#mode-dock` and `#play-hud` are in `FURNITURE.BARE`. Every *new* overlay has to be added to one of those two lists on the day it lands, because a frame with a stray card in it still renders and still looks deliberate. The mode dock proved it: it shipped inside the tera share card for a fortnight and nobody noticed. - **The two capture scripts both serve on port 5210.** Two of them at once is `EADDRINUSE` a minute into a run, so do not start a film while a stills pass is going — or give one of them `--site`-style isolation first. - Commit tera first, then re-run, so the manifest records a clean sha. ## The Bay Area frame drop was the GPU, not the scene — closed Struck 2026-08-22. This section used to record a p95 of 33.3 ms on `bay-area` desktop as a real defect and carried a 33.4 ms allowance in `scripts/performance-budgets.json`. **Both were wrong, and the way they were wrong is the lesson.** The shadow-map hypothesis is measurably false: 4096, 2048, 1024 and 256 all render the board in 1.21-1.31 ms. And the frame-time metric is not reproducible on this box at single-run granularity — two consecutive runs over a byte-identical `dist` gave 33.4 then 16.7 on the same cell, with triangle and draw counts identical to the digit. The card was sitting at **500 MHz of a possible 2725** through every run that reproduced the drop. So it was GPU power management, and the "it reproduces" conclusion rested on two samples. Two draws from a bimodal metric is not reproduction. The allowance is removed and the cell is back to 16.7. **Geometry is the gate here; frame time is advisory.** Triangle and draw counts are deterministic and identical across runs. Treat a single red p95 as noise until it reproduces across several runs, and record the GPU clock state next to the renderer string so the next person sees what this one did not. ## The aeroplane glyph is still larger than the Golden Gate `GLYPH_MAX_SCALE` in `src/engine/flights.ts` is 52 and that is a mitigation, not a cure. The glyph has a screen-space *floor* — never smaller than legible — which scales by the distance to the **aircraft**, when what makes it look wrong is how far the camera is from **what it is looking at**. At a whole-board pose those are the same thing; at the Golden Gate chapter the bridge is two units from the camera and the traffic is two thousand, so the floor fires hard on the aeroplane and not at all on the bridge. A ceiling of 52 was chosen because the raw scale at 1,160 units — the far end of the orbit over the California corridor — is 51.0 at a 60-degree field of view, so anything lower shrinks aeroplanes at a pose people actually use. (26 was tried first and put the glyph at 0.0123 of the frame, against the 0.012 at which `flights.ts` says the wings stop resolving.) 52 takes the worst case from about two and a half times the bridge's main span down to about one and a half. The complete fix is to clamp against the camera's focus distance rather than the aircraft's, so the glyph collapses toward its authored size whenever the viewer has zoomed in on something near, at any aircraft range. That is a signature change through `glyphScale`, `tick` and their callers.