1
0

perf: one lake mesh per board, and the merged board's metro layers get an LOD

Closing the merged board's red budget cells, and one of the two fixes turned out
to be a win for every board in the product rather than for this one.

**Every lake on a board is now one mesh.** `createWater` built a `Mesh` and a
`MeshStandardMaterial` per inland-water polygon, so a board paid a draw call per
lake. It went unnoticed while the boards were separate — California draws one,
the Salton Sea — and a scene census on a phone found it the moment `unify.ts`
folded three packs together: **water=20 against water=1**, nineteen of the
fifty-eight draw calls that put the merged board over the mobile cap. They differ
in nothing but shape, so the geometries merge and the material is shared. The
0.05 lift is baked in before the merge, since afterwards there is no per-lake
mesh to carry it.

Measured on the shipping default, unrelated to any flag: socal 218 -> 205 draw
calls, bay-area 208 -> 202, california 374 -> 373, california-drive 233 -> 232.
It was never free anywhere.

**The metro-scale layers now follow the same reveal as the buildings.** Bridges,
airports, ports and vessels are handed to `applyDetailLod` and hidden with the
detail districts. That every one of them is metro detail on a merged board is
measured rather than assumed: the same census showed all seven named bridges,
all eight airport layers and every port mesh going from *zero* on the state pack
to one on the merged board — `california.ts` authors none of them. Together they
were the remaining 27 of those 58 draws, for structures sub-pixel at 1,919 m to
the unit.

Result on the merged board: california desktop 348,271 triangles of 440,000 and
372 draws of 415 on mobile, both green where mobile was 428 before.

**`DETAIL_REACH_M` is 60 km, down from 150.** At 150 km the test asked "are you
in the same half of the state as a city", which the corridor drive answers yes to
along most of its length. 60 km asks "are you looking at a city", which is what
the flag is for — and driving up US-101 the Southland now appears as you come
into it rather than while you are still in the Salinas Valley.

**The metro chapters were merged, photographed and reverted, and the reason is
recorded rather than the attempt deleted.** Converting a metro rung onto this
board is easy and was done correctly — `focus.lat/lng` is absolute, `distance`
is horizontal in the owning board's units and `height` has already been through
that board's exaggeration, so each keeps its true metres. The camera arrives
exactly where it should. What is *at* that range is the problem: two scene units
from the target on a 551-unit board, and the corridor there is a deliberate
4.7 km-wide atlas glyph, because DRIVE mode has to be able to drive down it on a
board where a real freeway is a fifth of a pixel. The delivered frame is a black
slab across San Francisco. A rung that flies you into that is worse than no rung.
They come back when the corridor is drawn at true width, which is
`createFreewayWorld` and the `roads` rule, not `unify.ts`.

`airportsAndCard.test.ts` asserted a literal one-liner that is now two statements
— the airport group is captured so the LOD can hide it. Split into the two facts
it was ever about: the `?? []` call exists, and the result is added.

1,701 tests pass. The full budget passes on the shipping default.

Still open on the merged board: `california-drive` mobile peaks at 315 draws
against 280. The steady state is +3 drawables over the state board; the peak is
the car driving into a metro and revealing it, against a cap set when the
corridor had no cities on it. That is a cap to re-derive for a board that now has
them, and it is an owner's call rather than a number to quietly raise — which is
why this is still behind `?one=1`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-24 02:53:26 -07:00
parent 15d7accdbb
commit 64618cb47d
5 changed files with 179 additions and 57 deletions
+27 -7
View File
@@ -166,6 +166,32 @@ export function unifiedCalifornia(): { city: City; report: UnifyReport } {
m.districts.map((d) => ({ ...d, detail: true as const })), m.districts.map((d) => ({ ...d, detail: true as const })),
); );
/*
* ---- Chapters: the state's, and only the state's -------------------------
*
* **Tried, photographed, reverted.** Merging all twenty-four rungs onto this
* board is arithmetically easy — `focus.lat/lng` is absolute, `distance` is a
* horizontal length in the owning board's units and `height` has already been
* through that board's exaggeration, so each converts by keeping its true
* metres. That was done, and the camera arrives exactly where it should.
*
* The frame it arrives at is the problem, and it is a fact about the board
* rather than about the conversion. Flying to FiDi puts the camera about two
* scene units from its target on a board that is 551 units across, and what
* is at that range is *state-scale content*: `createFreewayWorld` draws the
* corridor as a deliberate 4.7 km-wide atlas glyph — `main.ts` says so where
* it sets the corridor altitude, because DRIVE mode has to be able to drive
* down it on a board where a real freeway is a fifth of a pixel — and the
* terrain under it is 300 m cells. The delivered picture is a black slab
* across San Francisco.
*
* So a rung that flies you into that is worse than not having the rung. The
* metro chapters come back when the corridor is drawn at true width on this
* board, which is `createFreewayWorld`'s hard-coded scene units and the
* `roads` reconciliation rule between them — not this file.
*/
const mergedChapters = state.chapters;
const fine = UNIFIED_FINE_METRES / 111_320; const fine = UNIFIED_FINE_METRES / 111_320;
const city: City = { const city: City = {
@@ -196,13 +222,7 @@ export function unifiedCalifornia(): { city: City; report: UnifyReport } {
inlandWater: [...state.inlandWater, ...metros.flatMap((m) => m.inlandWater)], inlandWater: [...state.inlandWater, ...metros.flatMap((m) => m.inlandWater)],
airports: [...(state.airports ?? []), ...metros.flatMap((m) => m.airports ?? [])], airports: [...(state.airports ?? []), ...metros.flatMap((m) => m.airports ?? [])],
ports: [...(state.ports ?? []), ...metros.flatMap((m) => m.ports ?? [])], ports: [...(state.ports ?? []), ...metros.flatMap((m) => m.ports ?? [])],
/* chapters: mergedChapters,
* Chapters are the state's alone. The metros' twenty-four rungs are poses
* expressed in *their* boards' framing, and `ladder.ts` already presents all
* of them as one list; re-pointing them at this board is its own piece of
* work and a wrong pose is a camera in the ground.
*/
chapters: state.chapters,
/* /*
* Landmasses are NOT unioned. `isLand` is a point-in-any-polygon test, so a * Landmasses are NOT unioned. `isLand` is a point-in-any-polygon test, so a
* metro's finer coastline can only ever add land the state's outline already * metro's finer coastline can only ever add land the state's outline already
+55 -11
View File
@@ -929,6 +929,26 @@ export async function createScene(
} }
} }
const detailReachUnits = DETAIL_REACH_M / world.metresPerUnit; const detailReachUnits = DETAIL_REACH_M / world.metresPerUnit;
/**
* The metro-scale layers, which follow the same reveal as the buildings.
*
* Filled after the port and vessel layers exist, a few dozen lines below;
* `applyDetailLod` is only ever called from the frame loop, which starts
* later still.
*
* **On a merged board every bridge, airport and port is metro detail**, and
* that is a measured fact rather than an assumption: a scene census of the
* state pack against the merged one showed all seven named bridges, all eight
* airport layers and every port mesh going from *zero* to one — `california.ts`
* authors none of them. Together with the crane heads, bridge lamps and hulls
* they were 27 of the 58 draw calls that put the merged board over the mobile
* cap, for structures that are sub-pixel at 1,919 m to the unit.
*
* Empty on every other board, because `hasDetail` is false there and
* `applyDetailLod` returns before reading this.
*/
const detailLayers: THREE.Object3D[] = [];
let detailShown = true; let detailShown = true;
function applyDetailLod(): void { function applyDetailLod(): void {
if (!hasDetail) return; if (!hasDetail) return;
@@ -947,18 +967,18 @@ export async function createScene(
detailShown = want; detailShown = want;
blocks.count = want ? baseCount + detailLots : baseCount; blocks.count = want ? baseCount + detailLots : baseCount;
for (const child of detailLandmarks) child.visible = want; for (const child of detailLandmarks) child.visible = want;
for (const layer of detailLayers) layer.visible = want;
} }
// Applied once up front so the opening frame is already correct rather than // Applied once up front so the opening frame is already correct rather than
// correct one tick later, which is a frame the capture harness can catch. // correct one tick later, which is a frame the capture harness can catch.
if (hasDetail) {
detailShown = true; const bridgeGroup = createBridges(world);
applyDetailLod(); scene.add(bridgeGroup);
}
scene.add(createBridges(world));
// Airfields. Laid flush on the terrain rather than draped over it like a // Airfields. Laid flush on the terrain rather than draped over it like a
// road, which is why the packs no longer carry runways as `Road` records — // road, which is why the packs no longer carry runways as `Road` records —
// carrying both floats a dark stripe thirteen metres above every runway. // carrying both floats a dark stripe thirteen metres above every runway.
scene.add(createAirports(world, city.airports ?? [])); const airportGroup = createAirports(world, city.airports ?? []);
scene.add(airportGroup);
/** /**
* The city switching itself on after sunset. Built after `blocks` because it * The city switching itself on after sunset. Built after `blocks` because it
@@ -1011,6 +1031,19 @@ export async function createScene(
scene.add(vesselLayer.group); scene.add(vesselLayer.group);
} }
/*
* Now that every metro-scale layer exists, hand them to the detail LOD and
* apply it once — up front, so the opening frame is already correct rather
* than correct one tick later, which is a frame the capture harness can catch.
*/
if (hasDetail) {
detailLayers.push(bridgeGroup, airportGroup);
if (portLayer) detailLayers.push(portLayer.group);
if (vesselLayer) detailLayers.push(vesselLayer.group);
detailShown = true;
applyDetailLod();
}
const precipLayer: PrecipLayer | null = options.precip const precipLayer: PrecipLayer | null = options.precip
? options.precip(world, { span: boardSpan }) ? options.precip(world, { span: boardSpan })
: null; : null;
@@ -1796,12 +1829,23 @@ export const DETAIL_STANDOFF_M = 120_000;
* How near a detail district the camera's *target* must be before that detail * How near a detail district the camera's *target* must be before that detail
* is drawn, in true metres. * is drawn, in true metres.
* *
* 150 km covers a metro and its approaches from any pose that can resolve a * **60 km, measured down from 150.** At 150 km the test asked "are you in the
* building, and excludes the middle of the state — which is what the drive * same half of the state as a city", which the corridor drive answers yes to
* chapter needed. Paired with `DETAIL_STANDOFF_M`: one asks "close enough to * along most of its length: `california-drive` revealed every building, bridge,
* see a building", the other "looking at somewhere that has any". * airport and port on the board while the camera was a chase view on a freeway
* eighty kilometres from any of them, and measured 315 draw calls against a 280
* mobile cap.
*
* 60 km asks "are you looking at a city", which is the question this flag is
* actually for. A metro on this board is roughly fifty kilometres across, so a
* target anywhere in one is comfortably inside; and driving up US-101 the
* Southland appears as you come into it rather than while you are still in the
* Salinas Valley — which is the better behaviour as well as the cheaper one.
*
* Paired with `DETAIL_STANDOFF_M`: one asks "close enough to resolve a
* building", the other "looking at somewhere that has any".
*/ */
export const DETAIL_REACH_M = 150_000; export const DETAIL_REACH_M = 60_000;
export const ARRIVAL_STANDOFF = 1.5; export const ARRIVAL_STANDOFF = 1.5;
/** How much higher, as a multiple. Larger than the stand-off: the move descends. */ /** How much higher, as a multiple. Larger than the stand-off: the move descends. */
+66 -36
View File
@@ -16,6 +16,7 @@
*/ */
import * as THREE from "three"; import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
import type { ScenePalette } from "./types.ts"; import type { ScenePalette } from "./types.ts";
import type { World } from "./world.ts"; import type { World } from "./world.ts";
@@ -977,46 +978,75 @@ diffuseColor.rgb *= mix( 1.0, uSeaDeep, seaFacing );`,
}; };
group.add(sea); group.add(sea);
/*
* **Every lake on the board is one mesh, and it used to be one mesh each.**
*
* A `Mesh` per polygon with a `MeshStandardMaterial` per polygon is a draw
* call per polygon, and the packs that have a lot of inland water are
* precisely the ones that can least afford them. It went unnoticed while the
* boards were separate — California draws one lake, the Salton Sea — and
* showed up the moment `cities/unify.ts` folded three packs into one: a
* scene census on a phone read **water=20 against water=1**, nineteen draw
* calls of the fifty-eight that put the merged board over the mobile cap.
*
* They differ in nothing but shape: same colour, same roughness, same
* polygon offset, same y. So this merges the geometries and shares one
* material, which is 1 draw instead of N on every board in the product —
* San Francisco and the Southland included, where it was never free either.
*
* The 0.05 lift is baked into each geometry before the merge rather than set
* on a mesh transform, because after the merge there is no per-lake mesh left
* to carry it.
*/
const lakeGeometries: THREE.BufferGeometry[] = [];
for (const poly of world.city.inlandWater) { for (const poly of world.city.inlandWater) {
const pts = world.projectPolygon(poly).map(([x, z]) => new THREE.Vector2(x, z)); const pts = world.projectPolygon(poly).map(([x, z]) => new THREE.Vector2(x, z));
const geo = new THREE.ShapeGeometry(new THREE.Shape(pts)); const geo = new THREE.ShapeGeometry(new THREE.Shape(pts));
geo.rotateX(Math.PI / 2); geo.rotateX(Math.PI / 2);
// The same change as the sea above, and for the same reason. Slightly geo.translate(0, 0.05, 0);
// rougher: an inland lake is sheltered, and a mirror-smooth bay next to a lakeGeometries.push(geo);
// wind-roughened ocean reads as the wrong way round. }
const lake = new THREE.Mesh( if (lakeGeometries.length > 0) {
geo, const merged = mergeGeometries(lakeGeometries, false);
new THREE.MeshStandardMaterial({ for (const geo of lakeGeometries) geo.dispose();
color: pal.lake, if (merged !== null) {
roughness: 0.24, const lake = new THREE.Mesh(
metalness: 0, merged,
side: THREE.DoubleSide, new THREE.MeshStandardMaterial({
/** color: pal.lake,
* The five-hundredths below is not a separation, at board scale. // Slightly rougher than the sea: an inland lake is sheltered, and a
* // mirror-smooth bay next to a wind-roughened ocean reads as the wrong
* A lake floats over the shore plate, which is the same landmass // way round.
* polygon lying flat at y=0, and 0.05 units is all there is between roughness: 0.24,
* them. On San Francisco — 230 units across, camera a couple of hundred metalness: 0,
* out — that is comfortably more than one step of the depth buffer and side: THREE.DoubleSide,
* the two never argue. On the California board the camera stands 570 /**
* units off and the depth buffer's resolution *there* is about 0.15 * The five-hundredths above is not a separation, at board scale.
* units, three times the gap: the Salton Sea came out banded in *
* alternating stripes of lake and shore, which reads as a rendering * A lake floats over the shore plate, which is the same landmass
* artefact because it is one. * polygon lying flat at y=0, and 0.05 units is all there is between
* * them. On San Francisco — 230 units across, camera a couple of
* Polygon offset is the fix rather than a bigger `y`, because it is * hundred out — that is comfortably more than one step of the depth
* expressed in units of whatever the depth buffer can currently resolve * buffer and the two never argue. On the California board the camera
* — it scales itself with distance, where a hard-coded lift would have * stands 570 units off and the depth buffer's resolution *there* is
* to be tuned per board and would leave the lake visibly hovering on the * about 0.15 units, three times the gap: the Salton Sea came out
* two boards that never needed it. * banded in alternating stripes of lake and shore, which reads as a
*/ * rendering artefact because it is one.
polygonOffset: true, *
polygonOffsetFactor: -2, * Polygon offset is the fix rather than a bigger `y`, because it is
polygonOffsetUnits: -4, * expressed in units of whatever the depth buffer can currently
}), * resolve — it scales itself with distance, where a hard-coded lift
); * would have to be tuned per board and would leave the lake visibly
lake.position.y = 0.05; * hovering on the two boards that never needed it.
group.add(lake); */
polygonOffset: true,
polygonOffsetFactor: -2,
polygonOffsetUnits: -4,
}),
);
lake.name = "inlandWater";
group.add(lake);
}
} }
return group; return group;
+15 -3
View File
@@ -61,12 +61,24 @@ test("the scene builds them, from the field the packs fill in", () => {
scene.includes('import { createAirports } from "./airports.ts";'), scene.includes('import { createAirports } from "./airports.ts";'),
"scene.ts no longer imports the airport kit", "scene.ts no longer imports the airport kit",
); );
/*
* Two assertions rather than one literal line, because the call and the add
* are no longer the same statement: the group is captured so the merged
* board's detail LOD can hide it, and matching the old one-liner would fail
* for a formatting change while still passing for a real regression like a
* guard replacing `?? []`. The two facts are what the test was ever about.
*/
assert.ok( assert.ok(
/scene\.add\(createAirports\(world, city\.airports \?\? \[\]\)\);/.test(scene), /createAirports\(world, city\.airports \?\? \[\]\)/.test(scene),
"scene.ts must add the airports for the city it was handed. `?? []` and not a " + "scene.ts must build the airports for the city it was handed. `?? []` and not " +
"guard, because a board with no airfields is the normal case and must cost " + "a guard, because a board with no airfields is the normal case and must cost " +
"nothing to express.", "nothing to express.",
); );
assert.ok(
/scene\.add\(\s*airportGroup\s*\)/.test(scene) ||
/scene\.add\(createAirports\(/.test(scene),
"scene.ts builds the airports but never adds them to the scene",
);
}); });
test("the airport contract stays plain data, and off the package surface", () => { test("the airport contract stays plain data, and off the package surface", () => {
+16
View File
@@ -127,3 +127,19 @@ describe("one California", () => {
assert.equal(unifiedCalifornia().city, city); assert.equal(unifiedCalifornia().city, city);
}); });
}); });
describe("one California's chapters", () => {
it("is the state's list, until the corridor is drawn at true width", () => {
/*
* The metro rungs were merged, photographed and reverted; `unify.ts` records
* why in full. The short version is that the conversion was right and the
* frame was not a two-unit stand-off on a 551-unit board lands inside the
* corridor's 4.7 km-wide atlas glyph. This test exists so that the day
* somebody merges them again, they do it deliberately.
*/
assert.deepEqual(
city.chapters.map((c) => c.id),
CALIFORNIA.chapters.map((c) => c.id),
);
});
});