1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/test/render/terrainLod.test.ts
T
karti 6fde2f0c97 feat: one California by default, and a box ship you can see
Two complaints, and the measured cause of each turned out to be a number
already written down in this repo rather than anything that needed inventing.

**"I see three different maps."** `reconcile.ts` has existed for a round with
the four rules that make three packs draw one California, behind a flag whose
whole purpose was that the owner could choose from a photograph — and
`DEFAULT_RULES` was empty, so nobody ever saw the reconciled board without
typing a query string. The photographs are now taken, at HEAD, at a matched
17:10 and a matched pose against the live API, and two of the four rules are on:

- `exaggeration`. Relief in frame is 7.24% on California, 6.92% on the Southland
  and **4.51%** on the Bay: two boards were already on one number and one was
  not. The rule moves SF and only SF, 3.60 -> 5.78, and leaves the two boards
  every marketing still was tuned on untouched to the digit. Unreconciled, the
  Bay draws Tamalpais, the Berkeley hills and Mount Diablo as low smeared
  mounds; at 5.78 they stand up, and the chapter's own caption — "the Diablo
  range closing the east" — is true of the picture for the first time.
- `projection`. Each pack squashed longitude by the cosine of its *own* centre,
  so California and the Southland disagreed about Riverside by 3,435 m and about
  their shared corner by 4,025 m: 3% of the frame at a 109 km stand-off, and a
  visible sideways slide in any transition showing both. One canvas cannot hold
  two answers for one place.

`ground` stays off: its palette mix pushes the Southland's flats yellow-olive
inland, because that pack's `flats` is off the one-parameter family in a
direction no mix can reach. Its own comment calls it the riskiest of the four
and the photograph agrees. `roads` stays off for the reason argued in the file —
it rewrites a column nothing reads on the one board where the complaint shows.

**"I don't see boats on our ports."** True, and by construction. `Crane`'s own
doc note already had the cause: at SoCal's 3.4x a 130 m gantry stands 1.13 units
while a 400 m ship is 1.02 units *long*. Exaggeration multiplies heights and a
length takes 1x, so a hull is the one object on this coast that the board
flattens while stretching everything around it — and eleven of fifteen hulls in
a working harbour are alongside a berth, where a moored hull has no wake by
design and so had nothing to be seen by.

`FREEBOARD_RATIO` had already named the fix: a laden ULCV "stands about thirty
metres out of the water **before you count the container stack, and the stack is
what makes the silhouette**". The stack was never drawn. `containerHull` draws
it — three bays with the forward one a tier down, eight tiers of high-cube at
0.52 of keel-to-deck depth — which buys height and nothing else. No length, no
beam: a new test holds the container solid inside the merchant hull's bounding
box in x and z, so a hull still cannot overhang the berth it lies in, and
`vesselScale.test.ts` is green to the digit.

It arrived through the seam `HULL_SHAPES` said it would — one entry, one case in
`hullGeometry`, one arm in `hullShape` — which is the only evidence that seam
was real rather than merely claimed.

**The band is a modulation map, and the first pass of it was not.** Painted at
`PORT_PALETTE`'s real box values under a container ship's own 0x93aac4 tint, the
stack photographed near black — darker than the hull under it. `deckAtlas`'s own
header had predicted that exact failure two paragraphs above where I was
editing. So hue comes from the tint like every other part of every other hull,
and the band carries relative value and *structure*: at the 25 px a stack draws
at, individual box hues average to one tone and what survives is the shadow
between two boxes. Adding a fourth band also had to move the other three, since
they occupied 0.02..0.98 with nothing to append to, so band edges are now
computed from one layout rather than typed as six literals — and the four
hardcoded `v` values in the bow triangles read from it too.

Three tests were pinned rather than changed: they measure packs as authored or
synthetic one-hill fixtures, and each had an implicit dependency on the flag
being off. Following `roadWidth.test.ts`, which already did this.

Verified: 1,690 tests pass. All ten performance-budget cells pass with no cap
raised — bay-area desktop 2,306,424 triangles of 2,600,000 (the taller board
keeping more through the height guard), socal +284 triangles and +1 draw call
for the container mesh, of 1,700,000 and 320. Frame time is not claimed either
way; this box's GPU never leaves 500 MHz of 2,725.

⚠ This changes the default, so lumbridgecorp.com's 21 stills and 4 films are now
pictures of a board the product no longer draws. `npm run refresh` is the whole
of that work and it is not optional. `?reconcile=0` asks for the old board.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:32:51 -07:00

310 lines
12 KiB
TypeScript

/**
* The visible terrain is decimated where the ground is flat, and this is what
* holds the decimation honest.
*
* Everything `lodPatches` does is invisible by construction and therefore
* invisible to review: a wrong tolerance, a wrong diagonal or a dropped land
* test all produce a mesh that builds, renders and passes every other test in
* this directory, and shows up only as a board that has quietly lost its
* coastline or grown a crack. The four facts below are the ones the pictures
* were checked against, and each of them is a number.
*
* The boards are synthetic and small — twenty cells a side — for the reason
* `seaAndTerrain.test.ts` gives: none of this is about California, and a real
* pack would couple a render test to a city's coastline.
*/
import assert from "node:assert/strict";
import test from "node:test";
import * as THREE from "three";
import { setReconcile } from "../../cities/reconcile.ts";
import { createTerrain } from "../../engine/terrain.ts";
import type { City, ScenePalette } from "../../engine/types.ts";
import { World } from "../../engine/world.ts";
/** A square island in the middle of a one-degree board, at 0.05° per cell. */
/*
* Every fixture in this file is a synthetic one-hill board built to isolate one
* LOD guard, and `exaggeration` is derived from a pack's own blended peak — so
* left on it would rescale a deliberately gentle 40 m swell into something the
* height guard fires on, and the colour guard this file is measuring would be
* swamped by it. The reconciliation is a statement about the three real packs;
* these are not packs.
*/
setReconcile(false);
const BASE: Omit<City, "hills"> = {
id: "lod-board",
name: "LOD Board",
center: { lat: 37, lng: -122 },
bounds: { minLat: 36.5, maxLat: 37.5, minLng: -122.5, maxLng: -121.5 },
latScale: 100,
verticalExaggeration: 2,
cellLat: 0.05,
cellLng: 0.05,
coastFalloff: 0.02,
/*
* The island's rim sits half a cell outside the lattice corners it wants, so
* its land cells run 4..15 on both axes. That is deliberate: the patch levels
* are aligned to their own multiple, and an island whose interior straddled
* the alignment would make this file a test of where the coast happens to
* fall rather than of whether flat ground collapses.
*/
landmasses: [
[
[36.65, -122.35],
[37.35, -122.35],
[37.35, -121.65],
[36.65, -121.65],
],
],
parks: [],
inlandWater: [],
districts: [],
landmarks: [],
bridges: [],
roads: [],
chapters: [],
};
/** Flat: no hills at all, so the whole island is one plane at sea level. */
const FLAT: City = { ...BASE, hills: [] };
/**
* Rough: a hill every other cell, which is the frequency the lattice itself is
* sized for. Nothing here may collapse, because a bilinear patch across two
* cells of this is wrong by most of a hill.
*/
const ROUGH: City = {
...BASE,
hills: (() => {
const hills: City["hills"] = [];
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
hills.push({
name: `h${i}-${j}`,
lat: 36.75 + i * 0.1,
lng: -122.25 + j * 0.1,
elevation: 600,
radius: 0.05,
});
}
}
return hills;
})(),
};
async function board(city: City): Promise<World> {
const world = new World(city);
assert.equal(await world.ready(), true, "the synthetic board failed to build a heightfield");
return world;
}
/** Triangles the surface would have had if every land cell were drawn alone. */
function cellByCellTriangles(world: World): number {
const { latSteps, lngSteps, land } = world.lattice();
const w = lngSteps + 1;
let cells = 0;
for (let i = 0; i < latSteps; i++) {
for (let j = 0; j < lngSteps; j++) {
const a = i * w + j;
if (land[a] && land[a + 1] && land[a + w] && land[a + w + 1]) cells++;
}
}
return cells * 2;
}
/** The ground the cell-by-cell surface covered, in square scene units. */
function cellByCellArea(world: World): number {
const { latSteps, lngSteps, lats, lngs, land } = world.lattice();
const w = lngSteps + 1;
let area = 0;
for (let i = 0; i < latSteps; i++) {
for (let j = 0; j < lngSteps; j++) {
const a = i * w + j;
if (!land[a] || !land[a + 1] || !land[a + w] || !land[a + w + 1]) continue;
const [x0, z0] = world.project(lats[i] as number, lngs[j] as number);
const [x1, z1] = world.project(lats[i + 1] as number, lngs[j + 1] as number);
area += Math.abs((x1 - x0) * (z1 - z0));
}
}
return area;
}
/**
* The footprint of a range of the index, in square scene units.
*
* Area rather than a cell list because that is the property the decimation has
* to preserve exactly: the patches cover the same ground, they just cover it
* with fewer triangles. A merge that swallowed a coastal cell, or a T-junction
* that left a gap, changes this number and nothing else.
*/
function footprint(geo: THREE.BufferGeometry, start: number, count: number): number {
const index = geo.getIndex() as THREE.BufferAttribute;
const pos = geo.getAttribute("position") as THREE.BufferAttribute;
let area = 0;
for (let at = start; at < start + count; at += 3) {
const a = index.getX(at);
const b = index.getX(at + 1);
const c = index.getX(at + 2);
// Twice the signed area of the triangle projected onto the ground plane.
area += Math.abs(
(pos.getX(b) - pos.getX(a)) * (pos.getZ(c) - pos.getZ(a)) -
(pos.getX(c) - pos.getX(a)) * (pos.getZ(b) - pos.getZ(a)),
) / 2;
}
return area;
}
function visibleTriangles(mesh: THREE.Mesh): number {
return mesh.geometry.drawRange.count / 3;
}
function casterTriangles(mesh: THREE.Mesh): number {
const geo = mesh.geometry;
return ((geo.getIndex() as THREE.BufferAttribute).count - geo.drawRange.count) / 3;
}
test("flat ground collapses and cell-scale relief does not", async () => {
const flat = await board(FLAT);
const rough = await board(ROUGH);
const flatMesh = createTerrain(flat);
const roughMesh = createTerrain(rough);
const flatBase = cellByCellTriangles(flat);
const roughBase = cellByCellTriangles(rough);
assert.ok(flatBase > 200, `the flat board is too small to be a test: ${flatBase} triangles`);
/*
* A plane is a plane at any resolution, so the flat island must come out at
* the coarsest level the patch list allows — a sixteenth of the cell-by-cell
* count in the interior, plus whatever the coast leaves unaligned.
*/
assert.ok(
visibleTriangles(flatMesh) < flatBase / 4,
`flat ground kept ${visibleTriangles(flatMesh)} of ${flatBase} triangles`,
);
/*
* And the opposite, which is the half that a too-loose tolerance would break
* silently: ground that moves every cell has to keep every cell. This is the
* failure that turns a mountain range into a bump map, and it is the reason
* the tolerance is a measured number rather than a large one.
*/
assert.ok(
visibleTriangles(roughMesh) > roughBase * 0.9,
`relief at lattice frequency was decimated to ${visibleTriangles(roughMesh)} of ${roughBase}`,
);
});
test("the collapsed surface covers exactly the ground the cells covered", async () => {
for (const city of [FLAT, ROUGH]) {
const world = await board(city);
const mesh = createTerrain(world);
const drawn = footprint(mesh.geometry, mesh.geometry.drawRange.start, mesh.geometry.drawRange.count);
const expected = cellByCellArea(world);
/*
* The coastline is the whole point of this assertion. A patch is only
* collapsed when every one of its lattice points is on land, so the set of
* ground covered is unchanged down to the last stair-step — and if a merge
* ever reached across the shore, or a T-junction left a hole, the area is
* where it shows.
*/
assert.ok(
Math.abs(drawn - expected) < expected * 1e-6,
`${city.id} covers ${drawn} square units against ${expected}`,
);
}
});
test("no point of the collapsed surface strays from the heightfield", async () => {
const world = await board(ROUGH);
const mesh = createTerrain(world);
mesh.updateMatrixWorld(true);
const { latSteps, lngSteps, lats, lngs, height, land } = world.lattice();
const w = lngSteps + 1;
const raycaster = new THREE.Raycaster();
const down = new THREE.Vector3(0, -1, 0);
const from = new THREE.Vector3();
let worst = 0;
let sampled = 0;
for (let i = 0; i <= latSteps; i++) {
for (let j = 0; j <= lngSteps; j++) {
const k = i * w + j;
if (!land[k]) continue;
const [x, z] = world.project(lats[i] as number, lngs[j] as number);
// Nudged inward, because a ray down the exact rim of the mesh is a
// coin toss between hitting the edge triangle and missing the board.
from.set(x + 1e-4, 10_000, z + 1e-4);
raycaster.set(from, down);
const hit = raycaster.intersectObject(mesh, false)[0];
if (!hit) continue;
sampled++;
worst = Math.max(worst, Math.abs(hit.point.y - world.metres(height[k] as number)));
}
}
assert.ok(sampled > 100, `only ${sampled} lattice points landed on the surface`);
/*
* `LOD_HEIGHT_TOLERANCE` is 0.1 scene units and the surface sits 0.012 above
* the heightfield to clear the shore plate, so 0.12 is the tolerance plus
* that lift plus a rounding allowance. This is the assertion that a raised
* tolerance has to walk past: the decimation may not move the ground.
*/
assert.ok(worst < 0.12, `the surface strays ${worst} scene units from the heightfield`);
});
test("a colour boundary the height test cannot see stops the merge", async () => {
/*
* The coast is flat and its colour is not. `groundColor` ramps `sand` into
* `flats` over the first three metres of elevation, which is a band the
* coastal falloff makes tens of cells wide and which no height tolerance
* loose enough to be useful can protect. So the same board is built twice:
* once with a palette whose beach and flats are the same colour, and once
* with them far apart. The second must keep more triangles, and the only
* mechanism that can produce that difference is the colour guard.
*/
const beach: Partial<ScenePalette> = { sand: 0xffffff, flats: 0x000000 };
const plain: Partial<ScenePalette> = { sand: 0x9d9c93, flats: 0x9d9c93 };
// A single broad, low hill: the island climbs through the sand ramp gently
// enough that the height test is happy everywhere.
const gentle: City["hills"] = [
{ name: "swell", lat: 37, lng: -122, elevation: 40, radius: 0.4 },
];
const flatColoured = await board({ ...BASE, hills: gentle, palette: plain });
const rampColoured = await board({ ...BASE, hills: gentle, palette: beach });
const a = visibleTriangles(createTerrain(flatColoured));
const b = visibleTriangles(createTerrain(rampColoured));
assert.ok(b > a, `the colour guard changed nothing: ${b} triangles against ${a}`);
});
test("the shadow caster is coarser than the surface and stands on the same ground", async () => {
const world = await board(ROUGH);
const mesh = createTerrain(world);
const geo = mesh.geometry;
const seen = visibleTriangles(mesh);
const cast = casterTriangles(mesh);
assert.ok(cast > 0, "the relief stopped casting a shadow");
/*
* The caster's floor is `SHADOW_CASTER_STRIDE`, so on ground rough enough to
* defeat every merge it is a quarter of the surface and never more. A caster
* that came out the same size as the surface would mean the stride had been
* lost and the depth pass was paying full price for the board.
*/
assert.ok(cast <= seen / 3, `the caster kept ${cast} triangles against ${seen} visible`);
// Same board, so the same island: the caster may be blockier at the rim, but
// it may not be somewhere else.
const seenArea = footprint(geo, geo.drawRange.start, geo.drawRange.count);
const castArea = footprint(geo, geo.drawRange.count, (geo.getIndex() as THREE.BufferAttribute).count - geo.drawRange.count);
assert.ok(
castArea <= seenArea * 1.0001 && castArea > seenArea * 0.5,
`the caster covers ${castArea} square units against the surface's ${seenArea}`,
);
});