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
Claude 0d7eb28bb5 feat: the land is a surface, not paint — standard material and a 220 m detail relief
The ground was `MeshLambertMaterial` with a vertex colour and no maps of any
kind, next to a sea that has been a `MeshStandardMaterial` with a swell map on
it since `createWater` was written. Two things came out of that gap.

The louder one is invisible in the code and obvious in a picture: three forwards
`scene.environment` only to `isMeshStandardMaterial`, so the whole PMREM sky
`environmentRig.ts` builds — sun lobe, `SKY_RADIANCE_SHARE`, the ground bounce —
was reaching the water, the ports and the vessels and not one photon of it was
reaching the state. The land has been paying the counterweight for it the whole
time: the day stops gave up about a tenth of `hemiIntensity` and a quarter of
`ambientIntensity` to make room for an environment term the ground could not
receive.

The quieter one is the facets. `lodPatches` collapses flat ground into patches
up to eight 300 m cells across, so at a close pose the state is kilometre-wide
triangles with one normal each, and nothing on them.

So: one `MeshStandardMaterial` at roughness 0.92, metalness 0, carrying a tiling
detail normal map built from `textures.ts`' own `tileableNoise` / `sampleField` /
`normalMapData` at a **220 m** repeat rather than the office's 2 m — which is
sub-pixel at every stand-off this board can reach, orbit floor included. 220 m is
legible from about 45 km down and mip-flat above about 110 km, and it is not
300 m because a tile the size of a terrain cell paints the same pattern on every
cell and lands its repeat on the facet edges it exists to hide.

Zero new draw calls, and that was the constraint: same 18 objects, same one
shared material, same one attribute triple, same one program switch.
`cost-at.mjs` reads 355,763 / 437 draws at the whole-board pose, unchanged.

Three shader edits, none of which the material has a dial for:

  1. The detail UV is `position.xz`, in the vertex shader. A real `uv` attribute
     would be 369 KB against the ground's current 1.66 MB and would have to be
     added as one object shared by all 18 geometries or the arrangement quietly
     duplicates it seventeen times. The plan projection is also the natural
     parameterisation of a heightfield.
  2. The tangent-space slope is scaled by the cosine of the *drawn* slope.
     `verticalExaggeration` is 15 here, so a real 10° hillside draws at 69° and a
     real 30° Sierra face at 83°, and a plan-projected tile on a face at θ is
     stretched 1/cos θ along the fall line — 2.8x and 8.7x. Scaling by cos θ
     restores the same bumps instead of a smear of vertical stripes, for a dot
     product against world up rather than the three fetches triplanar would cost.
  3. It fades out between 25 km and 70 km of view distance, as the sea's does at
     `SEA_CALM_NEAR`. That matters more here than for the water: the board's own
     budget pose is 400 km out, where a 220 m tile is 0.6 device pixels.

The shore plate goes standard with it, mapless. It is the one place on the board
where two materials meet edge to edge in the same colour, and a Lambert plate
against a standard ground would have put a step of about a fifth of the
hemisphere's diffuse contribution around every coastline in the state.

A handheld gets the material and the environment and not the map — "off" is *no*
map, the way `MaterialQuality`'s `low` rung is, because the cost is one fetch and
`getTangentFrame`'s three derivative pairs per fragment and a smaller map costs
exactly the same fetch.

`tileableNoise`, `sampleField` and `normalMapData` are exported from
`textures.ts` rather than reimplemented, and `normalMapData` takes the tile size
it converts against instead of reading the office's constant. Same reason that
file takes `fbm` from `engine/world.ts`: a second noise is a second thing to keep
tiling, and a second Sobel is a second place to get the flipY reasoning wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 19:49:15 -07:00

563 lines
22 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;
})(),
};
/**
* A board that **is** chunked: five degrees a side, so its ground spans more
* than one `TERRAIN_CHUNK_METRES` cell on both axes, with an island that leaves
* some cells empty.
*
* The one thing that makes it chunked is the `detail: true` district.
* `createTerrain` gates the split on `city.districts.some(d => d.detail)`, which
* `cities/unify.ts` is the only place in the repo that writes — so this fixture
* is standing in for the merged board and every other board in this file is
* standing in for the five that must not move.
*
* The district itself is never built here; `createTerrain` reads nothing from it
* but the flag.
*/
const CHUNKED: City = {
...BASE,
id: "chunked-board",
bounds: { minLat: 34.5, maxLat: 39.5, minLng: -124.5, maxLng: -119.5 },
center: { lat: 37, lng: -122 },
cellLat: 0.25,
cellLng: 0.25,
landmasses: [
[
[35.4, -123.6],
[38.6, -123.6],
[38.6, -120.4],
[35.4, -120.4],
],
],
hills: [
{ name: "north-ridge", lat: 38, lng: -123, elevation: 1_400, radius: 0.5 },
{ name: "south-ridge", lat: 36, lng: -121.4, elevation: 900, radius: 0.6 },
],
districts: [
{
id: "metro",
name: "Metro",
polygon: [
[36.9, -122.1],
[37.1, -122.1],
[37.1, -121.9],
[36.9, -121.9],
],
gridAngle: 0,
minHeight: 10,
maxHeight: 60,
towerChance: 0,
palette: "downtown",
detail: true,
},
],
};
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;
}
/**
* The one mesh an un-chunked board's terrain group holds.
*
* `createTerrain` returns a `THREE.Group` since the merged board's surface was
* split into frustum-cullable chunks. Every board in this file is synthetic and
* declares no `detail` districts, so every one of them takes the un-chunked
* branch and the group holds exactly one mesh — today's geometry, today's
* `seen`/`cast` packing, today's two shadow hooks. That is deliberate: these
* assertions were always about that path, and now they say so.
*/
function surface(group: THREE.Object3D): THREE.Mesh {
assert.equal(group.children.length, 1, "a board with no detail districts must not be chunked");
return group.children[0] as THREE.Mesh;
}
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 = surface(createTerrain(flat));
const roughMesh = surface(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 = surface(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 = surface(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(surface(createTerrain(flatColoured)));
const b = visibleTriangles(surface(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 = surface(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}`,
);
});
// ---- The frustum-cullable split -------------------------------------------
/*
* Everything below is about the merged board's terrain being several objects
* rather than one, and none of it was asserted anywhere before: three culls per
* *object*, so a single mesh whose bounding sphere contains California is
* submitted at every pose. The split is a re-index and nothing else — same
* vertices, same resolution, same normals — which is what makes these
* assertions checkable as identities rather than as tolerances.
*/
function chunksOf(group: THREE.Object3D): THREE.Mesh[] {
return group.children.filter((c) => c.name === "terrainChunk") as THREE.Mesh[];
}
function casterOf(group: THREE.Object3D): THREE.Mesh {
const found = group.children.filter((c) => c.name === "terrainCaster");
assert.equal(found.length, 1, `a chunked board must have exactly one caster, not ${found.length}`);
return found[0] as THREE.Mesh;
}
test("a board with detail districts is split into cullable chunks and one caster", async () => {
const world = await board(CHUNKED);
const group = createTerrain(world);
const chunks = chunksOf(group);
casterOf(group);
/*
* Bounded on both sides, and the upper bound is the point. A draw call is the
* scarce resource on the merged board — 420 against a 460 cap — so a grid
* that got finer would spend the budget before it saved a triangle.
* `MAX_TERRAIN_CHUNKS` is that bound stated as a number and this is what
* holds a future pack to it.
*/
assert.ok(
chunks.length >= 2,
`a board this size must chunk; it produced ${chunks.length} of them`,
);
assert.ok(
chunks.length <= 20,
`${chunks.length} chunks is more draw calls than the grid is allowed to spend`,
);
assert.equal(group.children.length, chunks.length + 1, "something else is in the terrain group");
});
test("the chunks together cover exactly the ground one mesh covered", async () => {
const world = await board(CHUNKED);
const chunks = chunksOf(createTerrain(world));
let drawn = 0;
for (const chunk of chunks) {
const index = chunk.geometry.getIndex() as THREE.BufferAttribute;
// The whole index, not a draw range: for a chunk the index *is* the range.
drawn += footprint(chunk.geometry, 0, index.count);
}
const expected = cellByCellArea(world);
/*
* The assertion that matters most in this file, and the one the single-mesh
* version of this test could not make: a patch dropped by the binning, or
* filed into two cells at once, shows here and nowhere else. It would not
* show in a picture either — a missing patch at 186 km to the chunk is a hole
* somewhere in the Central Valley that nobody is looking at. Do not loosen
* the tolerance.
*/
assert.ok(
Math.abs(drawn - expected) < expected * 1e-6,
`the chunks cover ${drawn} square units against ${expected}`,
);
});
test("every chunk carries its own bounding volume, and it is a small one", async () => {
const world = await board(CHUNKED);
const group = createTerrain(world);
const caster = casterOf(group);
const boardSphere = caster.geometry.boundingSphere as THREE.Sphere;
assert.ok(boardSphere, "the caster must carry the whole board's sphere");
for (const chunk of chunksOf(group)) {
const sphere = chunk.geometry.boundingSphere;
assert.ok(sphere, "a chunk with no assigned sphere is a chunk three will compute one for");
/*
* This is the regression test for the failure that has no symptom.
* `computeBoundingSphere` walks the *position attribute*, not the index, and
* every chunk shares one position pool covering the whole board — so a
* chunk left to compute its own sphere gets the board's, is culled by
* nothing, and silently restores the cost this whole split exists to
* remove, plus sixteen extra draw calls. It looks identical on screen.
*/
assert.ok(
sphere.radius < boardSphere.radius * 0.6,
`a chunk's sphere has radius ${sphere.radius} against the board's ${boardSphere.radius}: ` +
"it was computed over the shared position pool rather than assigned",
);
}
});
test("the chunks and the caster share one vertex buffer and one material", async () => {
const world = await board(CHUNKED);
const group = createTerrain(world);
const caster = casterOf(group);
const position = caster.geometry.getAttribute("position");
const colour = caster.geometry.getAttribute("color");
const normal = caster.geometry.getAttribute("normal");
for (const chunk of chunksOf(group)) {
// Identity, not equality. Duplicated vertex data would be a second upload
// of the same megabytes, and normals recomputed per chunk would put a
// lighting crease down every seam in the grid.
assert.equal(chunk.geometry.getAttribute("position"), position);
assert.equal(chunk.geometry.getAttribute("color"), colour);
assert.equal(chunk.geometry.getAttribute("normal"), normal);
}
const materials = new Set(group.children.map((c) => (c as THREE.Mesh).material));
assert.equal(materials.size, 1, "one material, or the split costs a program switch per chunk");
// `MeshStandardMaterial` since the ground gained a surface response, and the
// cast is restated rather than left saying Lambert: the one shared material
// is now a patched standard one, and the point of this assertion is that all
// eighteen objects still hold the *same* one.
const material = caster.material as THREE.MeshStandardMaterial;
assert.equal(material.shadowSide, THREE.BackSide, "the acne cure did not survive the split");
/*
* The detail map is on the shared material or it is on nothing. A per-chunk
* material would be seventeen program switches, and a caster with a cheaper
* material would be two — the `materials.size` assertion above is what
* forbids both, and this is the thing that would have tempted someone to
* break it.
*/
assert.ok(material.normalMap, "the shared ground material lost its detail relief");
});
test("the chunks are culled and the caster deliberately is not", async () => {
const world = await board(CHUNKED);
const group = createTerrain(world);
for (const chunk of chunksOf(group)) {
// A chunk in the depth pass would double-count: the caster already covers
// the whole board at a quarter of the triangles.
assert.equal(chunk.castShadow, false);
assert.equal(chunk.receiveShadow, true);
assert.equal(chunk.frustumCulled, true, "a chunk that is not culled is the whole cost back");
}
const caster = casterOf(group);
assert.equal(caster.castShadow, true);
assert.equal(caster.receiveShadow, false);
/*
* `WebGLShadowMap.renderObject` gates the depth pass on `!object.
* frustumCulled || _frustum.intersectsObject(object)` — the same machinery
* as the colour pass. A culled caster drops the ridge that is off screen and
* legitimately shadows into frame, so the shadow moves when the camera turns.
*/
assert.equal(caster.frustumCulled, false, "shadows would depend on where the camera looks");
});
test("the caster draws nothing in the colour pass and everything in the depth pass", async () => {
const world = await board(CHUNKED);
const caster = casterOf(createTerrain(world));
const geo = caster.geometry;
const index = geo.getIndex() as THREE.BufferAttribute;
assert.ok(index.count > 0, "the relief stopped casting a shadow");
/*
* Zero, not `Infinity`. `WebGLRenderer.renderBufferDirect` early-returns on a
* draw count of `< 0` or `Infinity` and **not** on 0, so this mesh costs
* exactly one zero-triangle draw call per frame at every pose — which is the
* price of a caster that is never culled, and it is one draw call.
*/
assert.equal(geo.drawRange.count, 0, "the caster is drawing in the colour pass");
const nothing = null as never;
caster.onBeforeShadow(
nothing, nothing, nothing, nothing,
geo, caster.material as THREE.Material, nothing,
);
assert.equal(geo.drawRange.start, 0);
assert.equal(geo.drawRange.count, index.count, "the depth pass is drawing part of the caster");
caster.onAfterShadow(
nothing, nothing, nothing, nothing,
geo, caster.material as THREE.Material, nothing,
);
assert.equal(geo.drawRange.count, 0, "the caster leaked into the colour pass");
});