feat: the merged board's cities are lotted like cities, and drawn one city at a time
Three findings, each of which changed the next.
**1. The metros on the merged board were lotted at 806 m.** `LOT` is fixed in
scene units — 0.42 — and `blocks.ts`'s own note already spelled out what that
means: a lot is 40 m on San Francisco's board, 164 m on the Southland's and
**806 m** on the statewide one. Put San Francisco's fifty-two districts *on* the
statewide board and they inherit 806 m. Lots go as the inverse square of their
size, so the Bay Area's ~84,000 buildings became about **two hundred**, and a
city rendered as a handful of grey slabs. That is the whole reason the merged
board's cities looked wrong.
A detail district now measures its lot in metres. 160 m, chosen against a
shipping reference rather than a feeling: it is what Southern California's own
board already builds itself at (0.42 x 390.6), so a metro on the merged board is
lotted about as finely as the Southland lots itself. Measured in the browser:
**59,166 building instances, of which 2,839 are the state's own and 56,327 the
two metros'.** Against roughly two hundred before.
**2. An all-or-nothing LOD could not carry them.** `InstancedMesh.count` draws
the first N instances, so "base first, detail last" can say "no cities" and
"every city" and nothing between. Driving into Los Angeles therefore revealed
San Francisco's fifty-two districts as well, four hundred kilometres away and
behind the camera: `california-drive` measured **925,854 triangles against a
430,000 cap**. Shrinking the lot to fit would have needed about 478 m, which
gives back the entire gain.
So `updateBlocksDetail` re-packs by district. Each detail district's lots and its
centre are recorded at build time; when the visible *set* changes — a handful of
times in a journey across California, not per frame — the visible districts are
copied into the front of the instance buffers and `count` moves. Three
attributes move together and must: the matrix, the colour and
`FACADE_ATTRIBUTE`, which is per-instance and decides which windows are lit;
moving matrices alone would light a tower's windows on a warehouse.
`california-drive` fell 925,854 -> **368,962**.
**3. Two bugs in that, both mine, both found by measuring rather than reading.**
- `DetailStore.key` remembers which districts the buffers hold, and started as
`""` — which is also the key an *empty* visible set produces. The first call,
at the whole-board pose where nothing should draw, compared equal, took the
early return and did nothing.
- `InstancedMesh`'s constructor sets `count` to capacity, so between
`createBlocks` returning and the scene's first `applyDetailLod` the mesh drew
every detail lot. The budget reports `maxTriangles` — a max over the whole
sample window — and caught that as **911,541 triangles while the steady
state was a correct 348,271**. A level of detail that is right on every frame
but the first is not a level of detail. The mesh is now born packed.
The second one is the instructive one: an instrumented log showed `count=2839,
want=false` at exactly the pose the budget was failing, which is what turned a
"the LOD is broken" hypothesis into "the LOD is right and the first frame is
not".
Result on the merged board, all triangles green: california 348,271 of 440,000 —
**lower than the state board's own 372,415** while carrying 56,327 more
buildings — and california-drive 368,962 of 430,000.
Still red, and unchanged from before this commit: `california-drive` mobile draw
calls, 315 against 280. Triangles there are now fine. That cap was set when the
corridor had no cities on it, and re-deriving it is an owner's call rather than a
number to quietly raise.
1,704 tests pass, including a regression test for the empty-key bug that reads
the source, because building a real `InstancedMesh` needs a GPU.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -143,3 +143,70 @@ describe("one California's chapters", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("one California's cities are lotted like cities", () => {
|
||||
it("measures a detail district's lot in metres, not in scene units", () => {
|
||||
/*
|
||||
* The defect this guards: `LOT` is fixed in scene units, which makes a lot
|
||||
* 40 m on San Francisco's board and **806 m** on the statewide one. Put
|
||||
* San Francisco's fifty-two districts on the statewide board unchanged and
|
||||
* they are lotted at 806 m — and lots go as the inverse square of their
|
||||
* size, so the Bay Area's ~84,000 buildings become about two hundred and a
|
||||
* city renders as a handful of grey slabs.
|
||||
*
|
||||
* Measured in the browser after the fix: the merged board carries 59,166
|
||||
* building instances, of which 2,839 are the state's own and 56,327 are the
|
||||
* two metros'. Before it, the metros contributed roughly two hundred.
|
||||
*
|
||||
* This asserts the property that produces that, rather than the count,
|
||||
* because the count moves with any pack edit and the property does not.
|
||||
*/
|
||||
const detail = city.districts.filter((d) => d.detail === true);
|
||||
assert.ok(detail.length > 0);
|
||||
// The state's own districts must NOT be marked, or they would be re-lotted
|
||||
// at 160 m across the whole of California and the board would never build.
|
||||
const base = city.districts.filter((d) => d.detail !== true);
|
||||
assert.ok(base.length > 0);
|
||||
for (const d of base) assert.equal(d.detail, undefined);
|
||||
});
|
||||
|
||||
it("keeps the metros' own boards untouched, which is the point of the flag", () => {
|
||||
// `unify` copies districts before marking them, so nothing it does can
|
||||
// reach back into `sf.ts` or `socal.ts` and re-lot the dedicated boards.
|
||||
for (const d of SAN_FRANCISCO.districts) assert.equal(d.detail, undefined);
|
||||
for (const d of SOCAL.districts) assert.equal(d.detail, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the detail repack", () => {
|
||||
it("distinguishes 'nothing packed yet' from 'nothing visible'", async () => {
|
||||
/*
|
||||
* A regression test for a bug this file's own author wrote and the budget
|
||||
* caught. `DetailStore.key` remembers which districts the instance buffers
|
||||
* currently hold, and it used to start as `""` — the same key an *empty*
|
||||
* visible set produces. So the first call, at the whole-board pose where
|
||||
* nothing should draw, compared equal to the initial state, took the early
|
||||
* return, and left `InstancedMesh.count` at the constructor's value, which
|
||||
* is every instance. Measured: 911,541 triangles against a 440,000 cap,
|
||||
* with the code meant to prevent it running and doing nothing.
|
||||
*
|
||||
* The distinction is the whole fix, so it is what is asserted — on the
|
||||
* source, because building a real `InstancedMesh` here would need a GPU.
|
||||
*/
|
||||
const source = await import("node:fs/promises").then((fs) =>
|
||||
fs.readFile("src/engine/blocks.ts", "utf8"),
|
||||
);
|
||||
assert.ok(
|
||||
/key: string \| null;/.test(source),
|
||||
"DetailStore.key must admit a value that no visible set can produce",
|
||||
);
|
||||
assert.ok(
|
||||
/key: null,/.test(source),
|
||||
"the store must start at that value, not at an empty key",
|
||||
);
|
||||
assert.ok(
|
||||
!/key: "",/.test(source),
|
||||
'an empty-string initial key is the bug: it equals the key of an empty visible set',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user