1
0

fix: the ground's detail was switched off everywhere, and the map was a grid of stars

Three defects in the round before this one, all of them found by rendering the
board and looking at it, none of them by a test that existed.

**1. The fade was wrong by a factor of nine, so the map did nothing.** It copied
`SEA_CALM_NEAR`'s form: `length( vViewPosition )` against a window converted from
real metres by `metresPerUnit`. That conversion assumes the camera's distance is
a plan measurement. On this board it is not — `verticalExaggeration` is 15, so a
camera lifted to 0.6 of its stand-off sits `sqrt(1 + (0.6 x 15)²) = 9.06`
stand-offs away in scene units — so a window written as "25 km to 70 km" closed
at about 2.8 km of stand-off and the detail was off everywhere a viewer can
actually go. Proved rather than guessed: a fivefold amplitude produced a
byte-identical PNG of the Central Valley.

It now measures the quantity instead of inferring it. `fwidth` of the detail UV
is exactly how much of one repeat a pixel covers, so its reciprocal is the
repeat's width in pixels — independent of board scale, exaggeration, field of
view, viewport and device pixel ratio, all five of which the old form was
guessing at. Off under 2 px, full over 8: the same thresholds the stand-off study
arrived at, without any of its assumptions. The geometric mean of the two axes
floored at an eighth of the larger, because that is what `anisotropy = 8`
resolves.

**2. The repeat went 220 m -> 440 m, and the recipe with it.** With the map
finally visible, the flat valley came out as a regular grid of four-pointed
stars, one per repeat, marching to the horizon. The cause is `tileableNoise` at a
`scale` of 2: below about 4, the blend's own wrapped cross-fade is the largest
feature in the tile. The layers now run 8 / 22 / 60 on a 128 lattice at 512², so
a repeat holds eight or so features instead of one and nothing in it is
identifiable. Mean slope 9.9°, peak 39.4°.

**3. The shore plate needed the map, not just the material.** A textured terrain
against a smooth plate makes the grid's stair-stepped rim legible — which is the
one thing `createShorePlates` exists to hide — and at San Francisco it came back
as a zigzag. Both now come out of one `groundMaterial`, which is two material
objects (the plate is a flat colour, the terrain a vertex ramp, and three
compiles those to different programs regardless) and the same one material per
mesh as before. The bytes of the detail map are built once and shared; the
`DataTexture`s are not, because `scene.ts` disposes a board by traversing it.

Zero new draw calls, unchanged: 437 at the whole-board pose, 348 at 7.7 km over
SF, 389 at 7.7 km over LA. Triangles unchanged to within the drift of the moving
vessels and aircraft.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-24 20:13:35 -07:00
parent 0d7eb28bb5
commit 76ca903cd2
3 changed files with 371 additions and 255 deletions
+57 -15
View File
@@ -276,6 +276,35 @@ test("the shore plate receives and does not cast", async () => {
assert.equal(plate.castShadow, false);
});
test("the shore plate wears the same surface as the ground it abuts", async () => {
const world = await board();
const plate = createShorePlates(world, { detail: true }).material as THREE.MeshStandardMaterial;
const ground = (createTerrain(world, { detail: true }).children[0] as THREE.Mesh)
.material as THREE.MeshStandardMaterial;
/*
* Two material objects — the plate is a flat `pal.shore` and the terrain is
* `vertexColors`, which three compiles to different programs whatever this
* file does — but every term that decides how they *light* has to agree,
* because the coastline is the one place on the board where two materials
* meet edge to edge in the same colour. A step there reads as a seam.
*
* Both halves were got wrong in turn and both were caught by a photograph.
* Lambert plate against a standard ground: `WebGLRenderer` forwards
* `scene.environment` only to `isMeshStandardMaterial`, so the rim sat about a
* fifth of the hemisphere's diffuse contribution darker than the ground.
* Mapless plate against a grained ground: the terrain grid's stair-stepped rim
* — which the plate exists to hide — came back as a legible zigzag between a
* textured surface and a smooth one at San Francisco.
*/
assert.ok(plate.isMeshStandardMaterial, "the coastline is lit by different terms again");
assert.equal(plate.roughness, ground.roughness);
assert.equal(plate.metalness, ground.metalness);
assert.ok(plate.normalMap, "the plate is smooth against grained ground, so the rim is legible");
assert.equal(plate.shadowSide, ground.shadowSide);
// Not the same *object*: it carries a colour where the ground carries a ramp.
assert.notEqual(plate, ground);
});
// ---- The land's surface ----------------------------------------------------
/*
@@ -380,26 +409,39 @@ test("the detail UV is built from the plan, because the ground has no uv attribu
// and 8.7x. Scaling the tangent-space slope by cos θ is what turns that back
// into the same bumps rather than a smear of vertical stripes, and it is the
// reason there is no triplanar projection here costing three fetches.
assert.match(shader.fragmentShader, /mapN\.xy \*= normalScale \* groundFlat \* \( 1\.0 - groundFar \);/);
assert.match(shader.fragmentShader, /mapN\.xy \*= normalScale \* groundFlat \* groundSeen;/);
// `fwidth` of the detail UV, and not a view distance. The distance form was
// written first and was wrong by a factor of nine on this board: a camera
// lifted to 0.6 of its stand-off sits 9.06 stand-offs away in scene units once
// `verticalExaggeration` has multiplied the lift, so a window stated in real
// metres closed at a ninth of the stand-off it named and the map was switched
// off everywhere a viewer could go. It was caught by a photograph, not by a
// test, which is why there is now a test.
assert.match(shader.fragmentShader, /fwidth\( vNormalMapUv \)/);
assert.match(shader.fragmentShader, /normal = normalize\( tbn \* mapN \);/);
const tile = shader.uniforms.uGroundTile?.value as number;
// Repeats per scene unit. One repeat is `GROUND_DETAIL_TILE_METRES` of real
// ground on every board, which is what keeps the grain the same physical size
// on a 94 m board and a 1,919 m one.
// on a 94 m board and a 1,919 m one. 440 rather than 220, and the difference
// was a photograph: at 220 `tileableNoise`'s own wrapped cross-fade was the
// biggest feature in the tile, and the Central Valley came out as a regular
// grid of four-pointed stars, one per repeat.
assert.ok(
Math.abs(tile - world.metresPerUnit / 220) < 1e-9,
`one repeat covers ${world.metresPerUnit / tile} m, not 220`,
Math.abs(tile - world.metresPerUnit / 440) < 1e-9,
`one repeat covers ${world.metresPerUnit / tile} m, not 440`,
);
const fade = shader.uniforms.uGroundFade?.value as THREE.Vector2;
assert.ok(fade.x > 0 && fade.y > fade.x, "the detail never fades, so it aliases at the horizon");
// Stated in metres of view distance and converted here, so the fade means the
// same thing on every board. The board's own budget pose is 400 km out, where
// a 220 m tile is 0.6 device pixels — the fetch has to be worth nothing there.
assert.ok(
Math.abs(fade.y * world.metresPerUnit - 70_000) < 1,
`the detail survives to ${Math.round(fade.y * world.metresPerUnit)} m`,
);
/*
* Device pixels of one repeat, not scene units — so the window means the same
* thing on a 94 m board and a 1,919 m one, at any field of view, at any
* viewport, at any device pixel ratio and under any vertical exaggeration. It
* is off under 2 px, where the mip chain has already flattened the map, and
* full over 8 px, where it is legible. The board's own budget pose is 400 km
* out, where a 440 m repeat is 1.3 px: the fetch has to be worth nothing
* there.
*/
assert.ok(fade.x >= 1 && fade.x < fade.y && fade.y <= 16, `the fade window is ${fade.x}..${fade.y} px`);
});
// ---- The ground's detail map ------------------------------------------------
@@ -455,8 +497,8 @@ test("the ground detail map tiles: the wrap is no sharper than the interior", ()
seam = Math.max(seam, normalAt(data, size, size - 1, y).distanceTo(normalAt(data, size, 0, y)));
}
/*
* California is 1,063 km across and this tile is 220 m, so the map is laid
* down about five thousand times along one edge of the board. A derivative
* California is 1,063 km across and this tile is 440 m, so the map is laid
* down about two and a half thousand times along one edge of the board. A derivative
* that does not wrap paints a grid over the entire state — which is the exact
* failure `tileableNoise`'s four-way blend exists to prevent, reused here
* rather than reimplemented.
@@ -492,7 +534,7 @@ test("the ground detail map lights the same at 128 as at 256", () => {
Math.abs(a - b) / a < 0.12,
`mean slope is ${a} at 128 and ${b} at 256: the per-metre conversion is gone`,
);
// And it is a real slope rather than a rounding error: about 8.5° mean on the
// And it is a real slope rather than a rounding error: about 9.9° mean on the
// shipped field, deliberately far under the board's own 15x exaggeration so
// the grain does not out-shout the landforms.
assert.ok(a > 0.05 && a < 0.4, `the ground's mean slope is ${Math.atan(a) * 57.3}°`);