1
0
Commit Graph

4 Commits

Author SHA1 Message Date
Claude 0d7eb28bb5 feat: the land is a surface, not paint — standard material and a 220 m detail relief
The ground was `MeshLambertMaterial` with a vertex colour and no maps of any
kind, next to a sea that has been a `MeshStandardMaterial` with a swell map on
it since `createWater` was written. Two things came out of that gap.

The louder one is invisible in the code and obvious in a picture: three forwards
`scene.environment` only to `isMeshStandardMaterial`, so the whole PMREM sky
`environmentRig.ts` builds — sun lobe, `SKY_RADIANCE_SHARE`, the ground bounce —
was reaching the water, the ports and the vessels and not one photon of it was
reaching the state. The land has been paying the counterweight for it the whole
time: the day stops gave up about a tenth of `hemiIntensity` and a quarter of
`ambientIntensity` to make room for an environment term the ground could not
receive.

The quieter one is the facets. `lodPatches` collapses flat ground into patches
up to eight 300 m cells across, so at a close pose the state is kilometre-wide
triangles with one normal each, and nothing on them.

So: one `MeshStandardMaterial` at roughness 0.92, metalness 0, carrying a tiling
detail normal map built from `textures.ts`' own `tileableNoise` / `sampleField` /
`normalMapData` at a **220 m** repeat rather than the office's 2 m — which is
sub-pixel at every stand-off this board can reach, orbit floor included. 220 m is
legible from about 45 km down and mip-flat above about 110 km, and it is not
300 m because a tile the size of a terrain cell paints the same pattern on every
cell and lands its repeat on the facet edges it exists to hide.

Zero new draw calls, and that was the constraint: same 18 objects, same one
shared material, same one attribute triple, same one program switch.
`cost-at.mjs` reads 355,763 / 437 draws at the whole-board pose, unchanged.

Three shader edits, none of which the material has a dial for:

  1. The detail UV is `position.xz`, in the vertex shader. A real `uv` attribute
     would be 369 KB against the ground's current 1.66 MB and would have to be
     added as one object shared by all 18 geometries or the arrangement quietly
     duplicates it seventeen times. The plan projection is also the natural
     parameterisation of a heightfield.
  2. The tangent-space slope is scaled by the cosine of the *drawn* slope.
     `verticalExaggeration` is 15 here, so a real 10° hillside draws at 69° and a
     real 30° Sierra face at 83°, and a plan-projected tile on a face at θ is
     stretched 1/cos θ along the fall line — 2.8x and 8.7x. Scaling by cos θ
     restores the same bumps instead of a smear of vertical stripes, for a dot
     product against world up rather than the three fetches triplanar would cost.
  3. It fades out between 25 km and 70 km of view distance, as the sea's does at
     `SEA_CALM_NEAR`. That matters more here than for the water: the board's own
     budget pose is 400 km out, where a 220 m tile is 0.6 device pixels.

The shore plate goes standard with it, mapless. It is the one place on the board
where two materials meet edge to edge in the same colour, and a Lambert plate
against a standard ground would have put a step of about a fifth of the
hemisphere's diffuse contribution around every coastline in the state.

A handheld gets the material and the environment and not the map — "off" is *no*
map, the way `MaterialQuality`'s `low` rung is, because the cost is one fetch and
`getTangentFrame`'s three derivative pairs per fragment and a smaller map costs
exactly the same fetch.

`tileableNoise`, `sampleField` and `normalMapData` are exported from
`textures.ts` rather than reimplemented, and `normalMapData` takes the tile size
it converts against instead of reading the office's constant. Same reason that
file takes `fbm` from `engine/world.ts`: a second noise is a second thing to keep
tiling, and a second Sobel is a second place to get the flipY reasoning wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:49:15 -07:00
karti 48ddfcdaec perf: the terrain is a grid of chunks, so three can cull it
`createTerrain` returned one mesh covering 32.50-42.05 N and -124.50 to
-114.00 W, and three frustum-culls per *object* — so at every pose, from
400 km and from 2.5, the merged board submitted all 86,400 of its visible
triangles. Standing over San Francisco it paid for the Mojave.

It now returns a `THREE.Group` named `terrain`, holding a 5 x 5 grid over
`city.bounds` — **17 non-empty chunk meshes**, eight cells being Pacific,
Nevada and Arizona — plus one un-chunked shadow caster. The geometry is
not touched: same patches, same resolution, same board-wide normals. Only
the index is cut up, which is what makes this culling and not level of
detail, and what sidesteps placement drift entirely — no ground is
re-derived, so nothing that was placed against it can move.

All 18 geometries share **one** `position`/`color`/`normal` attribute
triple: no duplicated vertices, one upload of exactly today's bytes, and
no lighting crease down any of the sixteen seams, since the normals were
averaged before the partition existed. The bounding volumes are therefore
assigned by hand — `computeBoundingSphere` walks the position attribute
and not the index, so a chunk left to compute its own would get
California's, cull nothing, and restore the whole cost with seventeen
extra draw calls and no visible symptom. `terrainLod.test.ts` pins that.

5 x 5 because a draw call is the scarce resource here, not a triangle.
The sweep is in the constant's own comment: 4x4 -> 5x5 buys 1,246
triangles per added draw, 5x5 -> 6x6 buys 573, 6x6 -> 8x8 buys 61. Five is
the last grid where a draw is worth more than a thousand triangles.

The caster stays one object with `frustumCulled = false`, deliberately:
`WebGLShadowMap` gates the depth pass on the same test as the colour pass,
so a chunked caster would drop a ridge that legitimately shadows into
frame the moment the camera turned. It sits at `drawRange(0, 0)` with the
hooks inverted, and `renderBufferDirect` early-returns on `< 0` and
`Infinity` but not on 0 — one zero-triangle draw call per frame, forever.

Gated on `city.districts.some(d => d.detail === true)`, which
`cities/unify.ts` is the only place in the repo that writes. `?city=sf`,
`?city=socal`, both `?one=0` California cells and `office` take the
unchunked branch and get today's single mesh inside a group: measured, the
socal board is identical to the draw call at three poses, and `?one=0`
identical to the digit at five.

Measured with `scripts/cost-at.mjs` at 37.7897, -122.3972, merged board,
before -> after (triangles / draws):

    400 km   355,759/414 -> 355,759/431     the whole board, all 17 drawn
    120 km   351,019/327 -> 321,057/340
     45 km   332,969/265 -> 270,373/273
    7.7 km   330,578/340 -> 222,482/343
    2.5 km   329,544/335 -> 215,598/338     -34.6%

and over Los Angeles at 2.5 km, 761,648 -> 335,186: a breach of the
400,000 cap that no budget cell poses at, now under it. The +17 draws at
the whole-board pose is the price, it is a bound and not a measurement
(there are 17 objects), and it lands at 437/433 against caps of 460/455.
No cap moved; the budgets file gains one sentence saying that geometry
submitted is now a function of the camera, and only ever downward.

What it does not fix: it is culling, not level of detail — a chunk 1% on
screen draws 100% of its triangles, and the ground under San Francisco is
still `UNIFIED_FINE_METRES` at 300 m against that metro's own 45. Nothing
streams; build time and resident memory are unchanged. ~157,000 triangles
at the close pose are sky, and after this they are three quarters of what
is left. `minimap.ts` still samples 160 x 160 across the whole board.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 18:47:05 -07:00
karti 6fde2f0c97 feat: one California by default, and a box ship you can see
Two complaints, and the measured cause of each turned out to be a number
already written down in this repo rather than anything that needed inventing.

**"I see three different maps."** `reconcile.ts` has existed for a round with
the four rules that make three packs draw one California, behind a flag whose
whole purpose was that the owner could choose from a photograph — and
`DEFAULT_RULES` was empty, so nobody ever saw the reconciled board without
typing a query string. The photographs are now taken, at HEAD, at a matched
17:10 and a matched pose against the live API, and two of the four rules are on:

- `exaggeration`. Relief in frame is 7.24% on California, 6.92% on the Southland
  and **4.51%** on the Bay: two boards were already on one number and one was
  not. The rule moves SF and only SF, 3.60 -> 5.78, and leaves the two boards
  every marketing still was tuned on untouched to the digit. Unreconciled, the
  Bay draws Tamalpais, the Berkeley hills and Mount Diablo as low smeared
  mounds; at 5.78 they stand up, and the chapter's own caption — "the Diablo
  range closing the east" — is true of the picture for the first time.
- `projection`. Each pack squashed longitude by the cosine of its *own* centre,
  so California and the Southland disagreed about Riverside by 3,435 m and about
  their shared corner by 4,025 m: 3% of the frame at a 109 km stand-off, and a
  visible sideways slide in any transition showing both. One canvas cannot hold
  two answers for one place.

`ground` stays off: its palette mix pushes the Southland's flats yellow-olive
inland, because that pack's `flats` is off the one-parameter family in a
direction no mix can reach. Its own comment calls it the riskiest of the four
and the photograph agrees. `roads` stays off for the reason argued in the file —
it rewrites a column nothing reads on the one board where the complaint shows.

**"I don't see boats on our ports."** True, and by construction. `Crane`'s own
doc note already had the cause: at SoCal's 3.4x a 130 m gantry stands 1.13 units
while a 400 m ship is 1.02 units *long*. Exaggeration multiplies heights and a
length takes 1x, so a hull is the one object on this coast that the board
flattens while stretching everything around it — and eleven of fifteen hulls in
a working harbour are alongside a berth, where a moored hull has no wake by
design and so had nothing to be seen by.

`FREEBOARD_RATIO` had already named the fix: a laden ULCV "stands about thirty
metres out of the water **before you count the container stack, and the stack is
what makes the silhouette**". The stack was never drawn. `containerHull` draws
it — three bays with the forward one a tier down, eight tiers of high-cube at
0.52 of keel-to-deck depth — which buys height and nothing else. No length, no
beam: a new test holds the container solid inside the merchant hull's bounding
box in x and z, so a hull still cannot overhang the berth it lies in, and
`vesselScale.test.ts` is green to the digit.

It arrived through the seam `HULL_SHAPES` said it would — one entry, one case in
`hullGeometry`, one arm in `hullShape` — which is the only evidence that seam
was real rather than merely claimed.

**The band is a modulation map, and the first pass of it was not.** Painted at
`PORT_PALETTE`'s real box values under a container ship's own 0x93aac4 tint, the
stack photographed near black — darker than the hull under it. `deckAtlas`'s own
header had predicted that exact failure two paragraphs above where I was
editing. So hue comes from the tint like every other part of every other hull,
and the band carries relative value and *structure*: at the 25 px a stack draws
at, individual box hues average to one tone and what survives is the shadow
between two boxes. Adding a fourth band also had to move the other three, since
they occupied 0.02..0.98 with nothing to append to, so band edges are now
computed from one layout rather than typed as six literals — and the four
hardcoded `v` values in the bow triangles read from it too.

Three tests were pinned rather than changed: they measure packs as authored or
synthetic one-hill fixtures, and each had an implicit dependency on the flag
being off. Following `roadWidth.test.ts`, which already did this.

Verified: 1,690 tests pass. All ten performance-budget cells pass with no cap
raised — bay-area desktop 2,306,424 triangles of 2,600,000 (the taller board
keeping more through the height guard), socal +284 triangles and +1 draw call
for the container mesh, of 1,700,000 and 320. Frame time is not claimed either
way; this box's GPU never leaves 500 MHz of 2,725.

⚠ This changes the default, so lumbridgecorp.com's 21 stills and 4 films are now
pictures of a board the product no longer draws. `npm run refresh` is the whole
of that work and it is not optional. `?reconcile=0` asks for the old board.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:32:51 -07:00
karti f81d5218d4 feat: SFO, LAX, both bridges, a road that reads as a road, and aeroplanes that move
**The aeroplanes were stuck because the wire could not describe motion.**
`WireAircraft` carried position, altitude and heading and nothing else, so the
client could only interpolate between the last two observations: every aircraft
replayed a segment it had already flown, arrived at the newest known point, and
sat still until the next poll landed five to fifteen seconds later. The feed had
the missing numbers the whole time and the server threw them away. Sampled live
from `api.adsb.lol/v2/point` while writing this — `gs` ground speed, `track`,
`baro_rate`, plus `r` registration and `t` type designator. They are on the wire
now in SI, aircraft dead-reckon along their own track and correct toward the
truth when a fix lands, and the click card an anonymous visitor gets says
"B739 · N68834". That last part is the enrichment FR24 was wanted for, obtained
from an ODbL feed we may actually republish.

**SFO and LAX exist.** A new `engine/airports.ts` composes an airport from
runways, taxiways, aprons and terminal masses, with markings drawn on a canvas
rather than modelled; the pattern of the runways is what the eye recognises from
altitude, long before any building does. SFO is the two crossing pairs on the bay
fill; LAX is the four parallels either side of the terminal horseshoe, plus the
Southland fields under the traffic that actually flies there.

**The Golden Gate and the Bay Bridge are those bridges.** One kit in
`engine/bridges.ts`, because a suspension bridge is a repeated tower, a catenary
main cable, a series of hangers and a deck — so both are configurations rather
than two private implementations. The Bay Bridge carries the real 2013 topology:
two suspension towers west of Yerba Buena, one east, then the piered causeway.
The freeway stopped being a wireframe overlay and became a road, with shoulders,
a median, and lane markings as texture.

**And the board got faster while all of that landed.** California went from
728,744 triangles and 562 draw calls to 391,169 and 371 — headroom from 2.8% to
47.8%. The Bay Area board is 506,550 triangles lighter than before this work.
Two things paid for it:

- `transmission: 0.08` on the aircraft cockpit glass. three.js runs a full
  transmission backdrop pass whenever any rendered material has transmission
  above zero, re-drawing the entire opaque scene into a second target every
  frame — so the city was rendering terrain, every block and every freeway piece
  TWICE. Measured by patching only that number in a copy of the built bundle:
  703,267 tris / 562 draws with it, 398,608 / 371 without. The material was
  already `transparent: true, opacity: 0.86`, so it was buying nothing.
- Flatness-adaptive terrain LOD, which collapses runs of lattice cells wherever
  the height and colour agree with the quad replacing them. The coastline is
  provably untouched — a patch collapses only when every point is on land and
  agrees about `park` — and a test asserts the drawn footprint matches the
  cell-by-cell area to 1e-6. `createTerrain` got *faster*: the vertices it stops
  emitting cost more than the flatness scan costs to run.

**The budget now watches the boards this was built on.** There was no `bay-area`
or `socal` cell — so SFO, LAX and both bridges all landed in frames nothing
measured, which is how a cap you do not have looks from the inside. Both are in
the matrix now with caps set from measurement, and the rationale lives in the
harness because JSON cannot hold a comment.

Two known defects ship with this, both recorded in TODO.md rather than hidden:

- `bay-area.desktop` drops about one frame in twenty (p50 16.7, p95 33.3). It is
  desktop-only and not fill rate — mobile runs the same 2.26 M triangles at a
  comparable pixel count and holds 16.7 flat — which points at the 2048 shadow
  map desktop uses against handheld's 1024. Measured at the commit before this
  work with the same harness: identical p95 33.3. Pre-existing, and invisible
  until the cell existed.
- The aeroplane glyph is still about 1.5x the Golden Gate's main span at chapter
  zoom, down from 2.5x. `GLYPH_MAX_SCALE` is 52 because the raw scale at the far
  end of the California orbit is 51.0 at a 60-degree field of view, and 26 —
  tried first — put the glyph at 0.0123 of the frame against the 0.012 where the
  wings stop resolving. The real fix is to clamp against the camera's focus
  distance rather than the aircraft's, which is a signature change.

Tests 1020 -> 1137. Typecheck, build, eight budget cells, no-binaries,
provenance, zero-config boot, dependency licences, arena source hashes and the
UI smoke across two viewports and two access tiers all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 05:06:12 -07:00