feat: one California as one pack, behind ?one=1
The nesting preview is gone and this replaces it. That one drew each metro's
built scene inside the state board's under a similarity transform; the transform
was right and two independently built terrains then occupied the same ground and
interpenetrated. `cities/unify.ts` is the version with no seam to stitch,
because the way to not have two grids is to not have two grids: one `City`
carrying the state's landform and both metros' detail, so the engine builds one
world, one lattice and one terrain mesh.
**The merge is mostly a union and twice it must not be.** Everything that
describes a place in a `City` is absolute lat/lng, so hills, districts,
landmarks, bridges, ports and airports concatenate. Two do not:
- `World.elevationAt` *blends* every hill covering a point, so leaving the
state's coarse hills under a metro's fine ones adds their heights and lifts
the whole Bay. 25 state hills are dropped where a metro replaces them.
- The state's 17 districts include coarse stand-ins for both metros against
the metros' 99 real ones, and both drawn is two cities in one place. 9 go.
Filtering is by centre-in-bounds, which is honest and not exact, and the file
says so rather than hiding it.
**The lattice was the whole question and the answer was measured, twice.**
`TODO.md` records a statewide lattice at metro fidelity as 23.7M points and
dead. That is true at San Francisco's own 45 m cell, because `buildAxis` refines
per *axis* — a plus, not a box — so two metros at opposite corners refine most
of the state. The blowup is a function of the fine cell and is gentle until 45 m,
so the first pass took 150 m / x8, or 1.23M points.
That rendered **1,114,226 triangles at the whole-board pose against a 440,000
cap**, and the useful part is where it did *not* come from: with every metro
building hidden, triangles fell by 43,000 and draw calls by 111. The buildings
were never the cost. Terrain was. So the fine cell is what a statewide board can
afford without terrain LOD, and that is **300 m / x16** — 0.18M points, 11.6x
finer than the state pack's 3,473 m cell and 6.7x coarser than San Francisco's
own board. Measured at that size: california 392,862 triangles of 440,000 and
california-drive 405,124 of 430,000, both green on desktop.
**And the detail LOD is the cheapest kind there is.** `createBlocks` now orders
every `detail` district's lots *last*, so hiding 99 districts' buildings is one
assignment to `InstancedMesh.count` — no second mesh, no second draw call, no
allocation, and nothing for `nightlights.ts` to learn, since it holds the same
mesh either way. Landmarks are one mesh each so theirs is a `visible` flag.
The reveal is two tests and needed both. Stand-off alone passes for the drive
chapter, whose camera sits a few tens of metres off the ground half way up the
state while being two hundred kilometres from the nearest building this governs
— measured, it failed `california-drive` on triangles for lots nobody could see.
So detail draws only when the camera is inside `DETAIL_STANDOFF_M` *and* its
**target** is within `DETAIL_REACH_M` of a detail district. The target rather
than the camera, because the target is what is being looked at.
`src/test/unify.test.ts` pins the two non-union merges, the detail marking and
the lattice size. Writing it turned up something worth keeping: **six hills are
named in both the state pack and a metro pack** — Marin Headlands, San Bruno
Mountain, Mount Diablo, Bolinas Ridge, Mount Gleason, Mount Wilson — so the
first version failed on San Francisco's own Marin Headlands, identified as a
state hill by its name. `ladder.ts` hit this with "Whole Board" on two boards.
A name is not an identity; the test keys on the object.
Known and not fixed: mobile draw calls are 428 of 415 and 315 of 280 on the
merged board, from the port, vessel and bridge layers that the state board never
carried before and that no LOD hides yet. That is why this is still a flag.
Default is untouched and the full budget passes with it off.
1,700 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+106
-1
@@ -866,7 +866,94 @@ export async function createScene(
|
||||
}
|
||||
const blocks = createBlocks(world, buildingReservations);
|
||||
scene.add(blocks);
|
||||
scene.add(createLandmarks(world, buildingReservations));
|
||||
const landmarkGroup = createLandmarks(world, buildingReservations);
|
||||
scene.add(landmarkGroup);
|
||||
|
||||
/**
|
||||
* Metro detail on a merged board, revealed by how far the camera is standing
|
||||
* off. **A no-op on every board that is not the merged one.**
|
||||
*
|
||||
* `cities/unify.ts` folds two metros into the state board, which takes it from
|
||||
* 17 districts to 107 and from 58 landmarks to well past that. Drawn
|
||||
* unconditionally that measured **1,157,802 triangles against a 440,000 cap**
|
||||
* and 542 draw calls against 480 — for buildings sub-pixel at 1,919 m to the
|
||||
* unit, which is to say for nothing. This is the level of detail that makes
|
||||
* one California affordable, and it is deliberately the cheapest kind there
|
||||
* is: `createBlocks` has already ordered every detail lot *last*, so hiding
|
||||
* them is one assignment to `InstancedMesh.count` — no second mesh, no second
|
||||
* draw call, no allocation, and nothing for `nightlights.ts` to know about,
|
||||
* since it holds the same mesh either way. Landmarks are one mesh each, so
|
||||
* theirs is a `visible` flag rather than a count.
|
||||
*
|
||||
* `DETAIL_STANDOFF_M` is 120 km because that is just outside the ladder's own
|
||||
* handover ceilings — 71.2 km for the Bay and 108.8 for the Southland — so
|
||||
* the city under the camera has already appeared by the time the old build
|
||||
* would have swapped boards. Above it the merged board draws exactly what the
|
||||
* state pack always drew.
|
||||
*/
|
||||
const baseCount = (blocks.userData.baseCount as number | undefined) ?? blocks.count;
|
||||
const detailLots = blocks.count - baseCount;
|
||||
const detailLandmarks = landmarkGroup.children.filter(
|
||||
(child) => child.userData.detail === true,
|
||||
);
|
||||
const hasDetail = detailLots > 0 || detailLandmarks.length > 0;
|
||||
|
||||
/*
|
||||
* Where the detail actually is, in scene units, so "close enough to draw the
|
||||
* cities" is a question about the cities and not only about altitude.
|
||||
*
|
||||
* Stand-off alone is not enough and the drive chapter is the proof: the
|
||||
* corridor camera sits a few tens of metres off the ground half way up the
|
||||
* state, which passes any altitude test while being two hundred kilometres
|
||||
* from the nearest building this flag governs. Measured that way the merged
|
||||
* board failed `california-drive` on triangles for lots nobody could see.
|
||||
*
|
||||
* One centre per detail district — 99 of them — and the test is the nearest.
|
||||
* That is 99 squared distances a frame against a saving of nearly eight
|
||||
* hundred thousand triangles, and it is computed from `controls.target`
|
||||
* rather than the camera because the target is *what is being looked at*: a
|
||||
* camera high over the Bay is looking at the Bay however far away it is.
|
||||
*/
|
||||
const detailCentres: { x: number; z: number }[] = [];
|
||||
if (hasDetail) {
|
||||
for (const district of world.city.districts) {
|
||||
if (district.detail !== true || district.polygon.length === 0) continue;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
for (const [pLat, pLng] of district.polygon) {
|
||||
lat += pLat;
|
||||
lng += pLng;
|
||||
}
|
||||
const [x, z] = world.project(lat / district.polygon.length, lng / district.polygon.length);
|
||||
detailCentres.push({ x, z });
|
||||
}
|
||||
}
|
||||
const detailReachUnits = DETAIL_REACH_M / world.metresPerUnit;
|
||||
let detailShown = true;
|
||||
function applyDetailLod(): void {
|
||||
if (!hasDetail) return;
|
||||
const standoff = kit.camera.position.distanceTo(kit.controls.target) * world.metresPerUnit;
|
||||
let want = standoff < DETAIL_STANDOFF_M;
|
||||
if (want) {
|
||||
const target = kit.controls.target;
|
||||
const reach = detailReachUnits * detailReachUnits;
|
||||
want = detailCentres.some((c) => {
|
||||
const dx = target.x - c.x;
|
||||
const dz = target.z - c.z;
|
||||
return dx * dx + dz * dz < reach;
|
||||
});
|
||||
}
|
||||
if (want === detailShown) return;
|
||||
detailShown = want;
|
||||
blocks.count = want ? baseCount + detailLots : baseCount;
|
||||
for (const child of detailLandmarks) child.visible = want;
|
||||
}
|
||||
// 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.
|
||||
if (hasDetail) {
|
||||
detailShown = true;
|
||||
applyDetailLod();
|
||||
}
|
||||
scene.add(createBridges(world));
|
||||
// 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 —
|
||||
@@ -1241,6 +1328,7 @@ export async function createScene(
|
||||
* camera starts from a standstill: a curve that began at full speed would
|
||||
* read as a cut followed by a glide.
|
||||
*/
|
||||
applyDetailLod();
|
||||
if (arrival !== null) {
|
||||
arrival.elapsed += dt;
|
||||
const t = Math.min(1, arrival.elapsed / ARRIVAL_SECONDS);
|
||||
@@ -1698,6 +1786,23 @@ const HERO_ELEVATION_DEG = 29;
|
||||
const HERO_DISTANCE = 0.9;
|
||||
|
||||
/** How much further out the camera stands before the move, as a multiple. */
|
||||
/**
|
||||
* The stand-off, in true metres, below which a merged board draws its metro
|
||||
* detail. Just outside the ladder's handover ceilings; see `applyDetailLod`.
|
||||
*/
|
||||
export const DETAIL_STANDOFF_M = 120_000;
|
||||
|
||||
/**
|
||||
* How near a detail district the camera's *target* must be before that detail
|
||||
* is drawn, in true metres.
|
||||
*
|
||||
* 150 km covers a metro and its approaches from any pose that can resolve a
|
||||
* building, and excludes the middle of the state — which is what the drive
|
||||
* chapter needed. Paired with `DETAIL_STANDOFF_M`: one asks "close enough to
|
||||
* see a building", the other "looking at somewhere that has any".
|
||||
*/
|
||||
export const DETAIL_REACH_M = 150_000;
|
||||
|
||||
export const ARRIVAL_STANDOFF = 1.5;
|
||||
/** How much higher, as a multiple. Larger than the stand-off: the move descends. */
|
||||
const ARRIVAL_LIFT = 2.2;
|
||||
|
||||
Reference in New Issue
Block a user