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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user