/** * The city is packed by district, and now by frustum as well. * * `createBlocks` returns one `InstancedMesh` for every anonymous building on the * board, and three culls per *object* — so that mesh's bounding sphere contains * California and nothing about it is ever rejected at any pose. Standing 2.5 km * over Los Angeles the merged board packed 48,081 detail lots and 2,839 base * lots, 509,200 triangles against a 400,000 cap, two thirds of them behind the * camera or off the sides. No budget cell stands there, so nothing had ever * measured it. * * `updateBlocksDetail` takes a frustum for that, and the four facts below are * what hold it honest: that a board which passes no frustum is untouched, that * one which does packs strictly less, that the pad is a pad, and that neither * can ever pack more instances than the mesh has room for. * * The board is synthetic for the reason `seaAndTerrain.test.ts` gives: none of * this is about California, and a real pack would couple this to a coastline. */ import assert from "node:assert/strict"; import test from "node:test"; import * as THREE from "three"; import { setReconcile } from "../../cities/reconcile.ts"; import { createBlocks, updateBlocksDetail } from "../../engine/blocks.ts"; import type { City, District } from "../../engine/types.ts"; import { World } from "../../engine/world.ts"; setReconcile(false); /** A square district, `half` degrees to a side, centred on (lat, lng). */ function district(id: string, lat: number, lng: number, half: number, detail?: true): District { return { id, name: id, polygon: [ [lat - half, lng - half], [lat + half, lng - half], [lat + half, lng + half], [lat - half, lng + half], ], gridAngle: 0, minHeight: 20, maxHeight: 120, towerChance: 0.05, palette: "downtown", ...(detail === true ? { detail } : {}), }; } const CITY: City = { id: "cull-board", name: "Cull Board", center: { lat: 37, lng: -122 }, bounds: { minLat: 36, maxLat: 38, minLng: -123, maxLng: -121 }, latScale: 100, verticalExaggeration: 2, cellLat: 0.05, cellLng: 0.05, coastFalloff: 0.02, landmasses: [ [ [36.1, -122.9], [37.9, -122.9], [37.9, -121.1], [36.1, -121.1], ], ], parks: [], inlandWater: [], // One base district and two metros, far enough apart that a camera over one // cannot see the other — which is the whole case being measured. districts: [ district("statewide", 37, -122, 0.08), district("north", 37.7, -122.6, 0.06, true), district("south", 36.3, -121.4, 0.06, true), ], landmarks: [], bridges: [], roads: [], chapters: [], hills: [{ name: "swell", lat: 37, lng: -122, elevation: 200, radius: 0.5 }], }; async function built(): Promise { const world = new World(CITY); assert.equal(await world.ready(), true, "the synthetic board failed to build a heightfield"); return world; } /** A camera looking straight down at (lat, lng) from `units` above it. */ function looking(world: World, lat: number, lng: number, units: number): THREE.Frustum { const [x, z] = world.project(lat, lng); const camera = new THREE.PerspectiveCamera(42, 1, 0.01, units * 10); camera.position.set(x, world.groundAt(lat, lng) + units, z); camera.lookAt(new THREE.Vector3(x, 0, z)); camera.updateMatrixWorld(true); camera.updateProjectionMatrix(); return new THREE.Frustum().setFromProjectionMatrix( new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse), ); } const REACH = 40; // scene units; both metros are inside this of their own centre test("without a frustum the packing is exactly what it always was", async () => { const world = await built(); const blocks = createBlocks(world); const capacity = (blocks.instanceMatrix.array.length / 16) | 0; assert.ok(capacity > 200, `the fixture is too small to be a test: ${capacity} lots`); /* * **Born packed at zero**, which is the one thing that changed about the * returned mesh: the base lots stopped being a fixed prefix, so `count` * cannot start at `baseCount` any more. It still starts strictly below * capacity, which is the only property that line ever needed. */ assert.equal(blocks.count, 0, "the mesh must not draw a district nobody has selected"); // Out of reach of either metro: the base district and nothing else. const [x, z] = world.project(37, -122); updateBlocksDetail(blocks, x, z, 0); const base = blocks.count; assert.ok(base > 0, "the base district vanished"); // In reach of both: every lot on the board, in one contiguous run. updateBlocksDetail(blocks, x, z, 1_000); assert.equal(blocks.count, capacity, "reach alone must still be able to draw the whole board"); assert.ok(base < capacity, "the fixture has no detail lots to cull"); }); test("a frustum packs strictly less, and never more than there is room for", async () => { const world = await built(); const blocks = createBlocks(world); const capacity = (blocks.instanceMatrix.array.length / 16) | 0; const [nx, nz] = world.project(37.7, -122.6); updateBlocksDetail(blocks, nx, nz, REACH); const everything = blocks.count; updateBlocksDetail(blocks, nx, nz, REACH, looking(world, 37.7, -122.6, 6)); const culled = blocks.count; assert.ok( culled < everything, `the frustum removed nothing: ${culled} against ${everything} in reach`, ); assert.ok(culled > 0, "the district under the camera was culled away as well"); assert.ok(culled <= capacity, `${culled} instances is past the end of the buffer`); /* * `InstancedMesh.boundingSphere` is computed once, lazily, over whatever * `count` held at the time and never again — so a sphere computed while the * camera stood over one metro would hide the other for the rest of the * session. `updateBlocksDetail` nulls it on every repack; this is that. */ assert.equal(blocks.boundingSphere, null, "a stale sphere would hide the city it was not built at"); }); test("the pad keeps a district that is just off the edge of frame", async () => { const world = await built(); const blocks = createBlocks(world); const [nx, nz] = world.project(37.7, -122.6); const view = looking(world, 37.7, -122.6, 6); updateBlocksDetail(blocks, nx, nz, REACH, view, 0); const bare = blocks.count; /* * The pad is a fraction of the stand-off and exists to cover one frame of * lag: `scene.ts` runs the level of detail before `kit.tick`, so the frustum * is last frame's. Handed a large enough stand-off it must admit a district * the bare frustum rejected — which is the mechanism, stated as the only * thing about it that can be asserted without pinning the constant. */ updateBlocksDetail(blocks, nx, nz, REACH, view, 5_000); assert.ok( blocks.count > bare, `the pad admitted nothing: ${blocks.count} against ${bare} with no pad`, ); }); test("a board with no detail districts is never repacked at all", async () => { const plain = new World({ ...CITY, districts: [district("statewide", 37, -122, 0.08)] }); assert.equal(await plain.ready(), true); const blocks = createBlocks(plain); const drawn = blocks.count; assert.ok(drawn > 0, "the plain board built nothing"); assert.equal(blocks.userData.detail, undefined, "a board with no metros must carry no store"); // Every argument, including a frustum that contains nothing: still a no-op. updateBlocksDetail(blocks, 0, 0, 0, looking(plain, 36.05, -122.95, 0.2), 1); assert.equal(blocks.count, drawn, "a board with no detail districts must not move"); }); /** * A district on high ground is culled by where it *is*, not by where its * footprint would be at sea level. * * This is a regression test for a real defect, and it is written as a separate * board because the fixture above cannot express it: that board exaggerates by * 2 and its tallest hill is 200 m, so every district's height above the ground * plane is a rounding error next to its own plan radius and a cull volume * pinned at y=0 contains it by accident. * * The merged California board exaggerates by 15. A lot's local y is the terrain * under it times that, so a district in the San Gabriel foothills stands * kilometres of scene height above y=0 while its plan radius stays a few units. * Tested against a sphere centred on the ground plane, **39 of the merged * board's 97 district ranges had lots outside their own cull volume** — the * worst by 4.78 units, 9.2 km — and the picture that produces is hillside * buildings vanishing while they are on screen. * * The board below reproduces that geometry rather than that city: one small * detail district on a 3,000 m peak, exaggerated 15x, so its lots sit about * forty units up on a footprint about three units across. A camera at the * summit looking at the summit contains every one of those lots and misses a * y=0 sphere entirely. */ const PEAK_CITY: City = { ...CITY, id: "peak-board", verticalExaggeration: 15, districts: [district("summit", 37, -122, 0.02, true)], hills: [{ name: "peak", lat: 37, lng: -122, elevation: 3_000, radius: 0.25 }], }; test("a district on high ground survives a frustum that contains it", async () => { const world = new World(PEAK_CITY); assert.equal(await world.ready(), true, "the peak board failed to build a heightfield"); const blocks = createBlocks(world); const store = blocks.userData.detail as { ranges: { r: number; y0: number; y1: number }[] }; assert.ok(store !== undefined && store.ranges.length > 0, "the peak board built no district"); /* * The fixture is only a test while this holds. If a pack edit ever flattens * this board, the assertion below would pass against a y=0 volume too and * would quietly stop testing anything. */ const range = store.ranges[0]!; assert.ok( range.y0 > range.r, `the fixture is not off the ground plane: y0 ${range.y0.toFixed(2)} against r ${range.r.toFixed(2)}`, ); /* * Aimed at the summit, and reaching only as far as the summit. * * `looking()` above aims every camera at `y = 0` with a far plane ten times * the stand-off, which is right for that board and useless here: it puts the * ground plane in the middle of the frustum, so a cull volume pinned to the * ground plane is contained whatever the terrain does and the defect cannot * be expressed. This camera hangs eight units over the peak, looks at the * peak, and stops twelve units short — so the lots at y 36.09-39.28 are in * frame and the plane at y = 0, thirty-seven units below them, is not. */ const [x, z] = world.project(37, -122); const summitY = world.groundAt(37, -122); const camera = new THREE.PerspectiveCamera(42, 1, 0.01, 12); camera.position.set(x, summitY + 8, z); camera.lookAt(new THREE.Vector3(x, summitY, z)); camera.updateMatrixWorld(true); camera.updateProjectionMatrix(); const view = new THREE.Frustum().setFromProjectionMatrix( new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse), ); updateBlocksDetail(blocks, x, z, REACH, view, 0); assert.ok( blocks.count > 0, "a district standing on a peak was culled by a frustum aimed straight at it", ); });