1
0

SoCal, the whole bay, a moon, and gates that actually run

Six agents in parallel, and the two city packs independently reported the same
blocker: `focusRegions` and `coarseFactor` existed on the `City` type and
nothing implemented them. Uniform lattices would have been 2.9M points for
Southern California and 3.7M for the expanded bay. Both packs were unloadable
as written.

`buildAxis` is the answer, and it is honest about its limits: refinement is per
axis, not per rectangle, so a focus region sharpens its whole row *and* its
whole column. Two regions at opposite corners refine nearly everything between
them. Measured, not guessed — the bay went 0.53M points with one region and
1.64M with three, for detail nobody is looking at from a board this wide. One
region each, coarse factor ten, and the builds land at 3.8 s and 2.3 s.

Then three things that were only ever right because San Francisco was the only
city. `maxDistance: 340` and a 170-unit shadow box were constants tuned for a
230-unit board; the bay is 1003 units across and the camera physically could
not retreat far enough to frame it. Fog distances were scene units pinned to
the same assumption. And `minVisibilityM` defaulted to 4.5 km of honest
weather, which over ninety-four kilometres of bay correctly hides three
quarters of it — the night view was a black rectangle for a completely
reasonable reason. All three now derive from the board.

The moon is a real ephemeris and its light is a deliberate lie: 1.15, against a
physical ratio of one to four hundred thousand. What is being reproduced is
what a moonlit night looks like on a screen in a lit room.

The CI gate caught itself, which is the part worth keeping. Port 8431 was
already held by a server from an earlier session, so the boot check polled a
healthy stranger while the process it started died on EADDRINUSE. It now
refuses to run rather than pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-05 03:13:32 -07:00
parent 8bcb391455
commit 44c5a79424
25 changed files with 9246 additions and 220 deletions
+50
View File
@@ -31,6 +31,36 @@ const PALETTES = {
industrial: [0xbdb5a8, 0xa89f92, 0xcac2b4, 0xb0a89a, 0x9c9488],
} satisfies Record<District["palette"], number[]>;
/**
* How commercial each palette's buildings are, 0..1.
*
* Read only by `nightlights.ts`, and the reason a night city looks like a city
* rather than like a uniform field of dots: an office floor is a continuous
* band of large windows with half of them left on all night, and a house is two
* small warm rectangles that go out. The number is the same fact the palette
* already encodes, which is why it is derived from it rather than authored
* again per district.
*/
const COMMERCIAL = {
downtown: 1,
residential: 0.12,
industrial: 0.45,
} satisfies Record<District["palette"], number>;
/**
* The name of the per-instance attribute `createBlocks` leaves on its geometry:
* `[commercial, seed]`.
*
* A vertex attribute rather than a field on `userData` because the only
* consumer is a shader, and this puts the data where the GPU already wants it.
* `createBlocks` writes it because `createBlocks` is what knows which district
* a given instance came out of; nothing else can recover that from the mesh.
*/
export const FACADE_ATTRIBUTE = "aFacade";
/** An independent stream for the facades; see where it is drawn from. */
const FACADE_SEED = 20_261;
interface Box {
x: number;
z: number;
@@ -40,6 +70,7 @@ interface Box {
h: number;
rot: number;
color: THREE.Color;
commercial: number;
}
function polygonBounds(poly: [number, number][]) {
@@ -65,6 +96,7 @@ export function createBlocks(world: World): THREE.InstancedMesh {
seedBase += 7919;
const palette = PALETTES[district.palette];
const commercial = COMMERCIAL[district.palette];
const angle = district.gridAngle;
const coverage = district.coverage ?? 0.88;
@@ -124,6 +156,8 @@ export function createBlocks(world: World): THREE.InstancedMesh {
h: world.metres(heightM),
rot: angle + (rand() - 0.5) * 0.03,
color: new THREE.Color(palette[Math.floor(rand() * palette.length)] ?? 0xd9d3c6),
// A tower is an office whatever district it landed in.
commercial: Math.min(1, commercial + (isTower ? 0.4 : 0)),
});
}
}
@@ -132,6 +166,22 @@ export function createBlocks(world: World): THREE.InstancedMesh {
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.translate(0, 0.5, 0); // pivot at the base, so y is ground level
// The per-instance facade data, drawn from a stream of its own.
//
// The obvious place for the seed is inside the placement loop, next to every
// other `rand()` — and putting it there would have been a mistake, because a
// scatter's draw sequence is load-bearing. One extra call shifts every
// subsequent draw, and the whole city would have rebuilt itself the first
// time anyone lit a window. A second stream costs nothing, is just as
// deterministic across reloads, and leaves the skyline exactly where it was.
const windows = seededRandom(FACADE_SEED);
const facade = new Float32Array(boxes.length * 2);
boxes.forEach((b, i) => {
facade[i * 2] = b.commercial;
facade[i * 2 + 1] = windows();
});
geometry.setAttribute(FACADE_ATTRIBUTE, new THREE.InstancedBufferAttribute(facade, 2));
const mesh = new THREE.InstancedMesh(geometry, new THREE.MeshLambertMaterial(), boxes.length);
mesh.name = "blocks";
mesh.castShadow = true;