Tower — Live World Generation
Status: IN PROGRESS. Accepted by the author (D-062). Phases 1–6 are built (D-063 to D-067, D-069, D-071), with a scenic tour between 4 and 5 (§19, D-068) and the Capital given an extent (D-070); phases 7–8 are not. This document replaces the terrain pipeline of D-019 and reverses D-006; see §11 for every existing decision it touches, and §15 for where Phase 1 departed from the plan and why.
0. Summary
Today the world is two images. npm run terrain reads content/geography.json,
bakes a 2048 × 1192 height raster and a colour raster (264 m per pixel), and the
interior view displaces one cylinder mesh by the first and paints it with the
second. Every lake, forest and desert is a colour in a PNG. Nothing is smaller than
a pixel, and nothing moves.
This proposal replaces that with features that generate themselves, live, in the browser:
| Feature | What gets generated |
|---|---|
| Mountains | named peaks at their authored heights, ridges and saddles between them, valleys cut by simulated fluvial erosion, snow and bare rock by altitude and slope |
| Lakes | a basin, a real water surface at a constant level, wind waves sized by fetch |
| Rivers | a valley and channel descending monotonically from source to outlet, a water surface that follows the descent, and flow animated downstream |
| Cap seas | the two ring oceans, with ocean waves and sailing ships on deterministic routes, both driven by Tower's published weather plan |
| Desert | dune fields, rocky hills, terraced plateaus and mesas, and the marks of water that was never there: dry washes, salt pans, abandoned shorelines |
| Forest | individual trees at real sizes, placed by a density field with planned stands and open meadows, and paths through it; a tall forest of 110 m giants that stands out as a landmark |
| Settlements | the capital, the stations, and generated hamlets, villages and towns filling out the world: a road skeleton, blocks, lots and blocky extruded buildings |
| Weather plan | Tower's published schedule of wind and storms per body of water, which it can revise at any moment |
| Roads & paths | authored or auto-routed along the terrain, grading the ground they cross, around the capital and through the forest |
There is no build step. The world file is the only input. The browser compiles it in web workers when the page loads and again whenever the author edits a feature, behind a status dialog that says what it is making (§10.1), and streams detail in around the observer as they fly through the interior view.
0.1 The premise every generator follows
Nothing inside Tower is a remnant. Tower is in full control of the inside, and every landform, forest, shoreline and wind is there because Tower chose to put it there — often because it saw the form on Earth, or on other planets, and judged it appropriate. So a dry wash in the Dry Reach is not evidence that water once ran there; it is a quotation. The generators still produce physically plausible forms — a wash still branches the way drainage branches — because Tower copies well. But nothing has to be consistent with a history, because there is none. (Author, 2026-09-16.)
1. Goals, non-goals, constraints
1.1 Goals
- Each concept is a first-class, authorable thing — placed on the map, given parameters, and regenerated when either changes.
- Scale runs from the world to your feet. 540 × 314 km is readable from 25 km up; a tree, a house and a path are readable standing at 1.7 m.
- Live. Changing a feature shows up in the map and the interior within seconds, without a terminal.
- Deterministic. The same world file produces the same world on every machine, every time. A reader's link to "stand here" shows the author's forest, not a reroll of it.
- Canon holds. Everything in
design.md§1 still applies: rivers descend because the floor was shaped (§1.9.3), level water is a surface of constant radius (§1.9.3), falling things lean antispinward (§1.9.2), haze is integrated (§1.8).
1.2 Non-goals (for this proposal)
- Simulating the world over time (erosion as a process you watch, growth, traffic). Erosion is a generation technique here, run once per edit.
- Clouds and rain are still not drawn (D-049 stands). Wind and waves are in scope, as Tower's published weather plan (§4.9).
- Interiors of buildings, people, vehicles other than ships.
- Photorealism. The target is "reads correctly and is honest about scale".
1.3 Constraints carried over
- Static hosting. GitLab Pages, no backend (§3.1). All generation is client-side.
- Phones. The app works below 720 px (D-043). Generation and rendering need a reduced tier, not a separate app (§8.4).
- Vanilla TypeScript, Three.js (D-005). No framework.
- Scene units are kilometres in both 3D views, so shaders read in canon numbers.
2. Terms
| Term | Meaning |
|---|---|
| (θ, z) | canon surface coordinate: θ radians around, z metres from the sun cap (§1.7) |
| arc | θ × R, metres around the circumference. (z, arc) is the unrolled plane, which is exact |
| elevation | metres above the datum cylinder, i.e. toward the axis |
| feature | one authored thing in the world file: a mountain range, a lake, a forest… |
| world compile | the whole-world pass that runs on load and on each edit (§5.2) |
| tile | one square-ish patch of the unrolled surface at one level of detail (§6.1) |
| field | a function of (θ, z): elevation, surface material, tree density… |
| scatter | discrete objects placed by a field: trees, buildings, rocks, ships |
3. The world model (schema v2)
content/geography.json and static/world.json merge into one file, since a
place and the terrain that makes it are no longer separate things. No backwards
compatibility: a one-off script ports today's geography into v2 so the authored
world is kept, and then both old formats are deleted.
3.1 Shape
As built in Phase 1, the schema differs from the sketch below in one structural way: every kind has the same
geometryfield (point, polyline, polygon or band), and the kind-specific parameters sit beside it — notarea,shore,courseandcentrefields per kind. One field meant the map, the list, the editor and the permalinks handle every kind one way. And each phase adds a kind's parameters only when a generator reads them, so Phase 1'sDeserthasupliftMandedgeM, not dunes;src/world/schema.tsis the current truth. The sketch stays as the target.
interface World {
version: 2
/** Changes every feature's randomness at once. Rarely touched. */
seed: number
ground: GroundSettings // the gentle base relief everywhere (today's "base" op)
features: Feature[]
/** Generated hamlets and villages between the authored places (§4.7). */
settlementDensity: number // per 1,000 km² of habitable land; 0 = only authored ones
weather: WeatherPlan // Tower's published schedule (§4.9)
}
interface FeatureBase {
id: string // permalink target; follows the name until pinned (D-012)
name: string
blurb: string // ships to readers
notes: string // never ships (D-011)
public: boolean
/** Written once at creation and mixed into the world seed (§5.5). Never derived from the id. */
seed: number
}
type Area =
| { type: 'polygon'; points: [theta: number, z: number][] }
| { type: 'band'; zFrom: number; zTo: number } // rings the world (D-027)
type Feature = FeatureBase & (
| Mountains | Lake | River | Sea | Desert | Forest | Settlement | Route | Marker
)
3.2 The concepts
Every parameter below has a default, so a feature can be dropped on the map with nothing but its geometry and look reasonable immediately.
interface Mountains {
kind: 'mountains'
area: Area // footprint; relief fades to the ground at its edge
spine?: Polyline // optional main ridge line; peaks are generated along it
peaks: Peak[] // authored summits, held at exactly their elevation
generatedPeaks: number // extra unnamed summits per 100 km² (default 0.6)
ruggedness: number // 0..1, how sharp ridges and deep valleys are
erosion: number // 0..1, how far the fluvial pass cuts in
snowlineM: number // default 3000
treelineM: number // default 2200
}
interface Peak { name?: string; theta: number; z: number; elevationM: number }
interface Lake {
kind: 'lake'
shore: Polygon
levelM: number | 'auto' // 'auto' = fill the basin to its lowest rim
depthM: number
wind: Wind // drives the waves (§4.2)
}
interface River {
kind: 'river'
course: Polyline // source first, mouth last
flowM3S: number // sets channel width and valley size
source: { elevationM: number | 'terrain' }
outlet: string | 'auto' // id of the lake / sea / river it ends in
valley: { widthM: number; depthM: number } | 'auto'
}
interface Sea {
kind: 'sea'
area: Area // the cap seas are bands; a sea can also be a polygon
levelM: number
depthM: number
wind: Wind
ships: { count: number; routes: 'ring' | 'coastal' | 'crossing' }
ice?: { edgeM: number; fastM: number; // D-090: open water, pack from edgeM, fast from fastM
range?: { fromM: number; heightM: number } } // D-093: ice mountains at the cap
}
interface Desert {
kind: 'desert'
area: Area
dunes: { coverage: number; heightM: number; wind: Wind }
hills: { coverage: number; heightM: number }
plateaus: { coverage: number; heightM: number; terraces: number }
pastWater: { washes: number; saltPans: number; shorelines: boolean }
}
interface Forest {
kind: 'forest'
area: Area
composition: { conifer: number } // 0 = all broadleaf, 1 = all conifer
giants: boolean // a tall forest: sequoia-class trees (§4.6)
densityPerHa: number // mature stems per hectare, default 450
stands: Stand[] // planned concentrations (§4.6)
meadows: { coverage: number; clearings: Polygon[] }
paths: { density: number } // auto-generated trail network, 0 = none
}
interface Stand { area: Polygon; densityPerHa: number; conifer: number; ageYears: number; layout: 'natural' | 'rows' }
interface Settlement {
kind: 'settlement'
size: 'hamlet' | 'village' | 'town' | 'station' | 'city' // the capital is 'city'
centre: { theta: number; z: number }
boundary: Polygon | { radiusM: number } | 'auto' // 'auto' sizes from `size`
roads: { arterials: number; pattern: 'grid' | 'radial' | 'organic'; blockM: number }
buildings: { coverage: number; storeys: [min: number, max: number]; peakAtCentre: boolean }
}
interface Route {
kind: 'road' | 'path'
/** Either drawn, or two endpoints routed automatically across the terrain. */
way: Polyline | { from: string; to: string }
widthM: number
}
/** Everything that is a label and not a landform: towns, embassies, landmarks, notes. */
interface Marker {
kind: 'embassy' | 'tower' | 'landmark' | 'note' | 'region'
geometry: Point | Polyline | Area
}
interface Wind { directionDeg: number; speedMS: number } // 0° = deepward, 90° = spinward
interface WeatherPlan {
/** The wind each body of water gets when nothing is scheduled. */
baseline: { target: string; wind: Wind }[]
events: WeatherEvent[]
}
interface WeatherEvent {
id: string
target: string // a sea, lake or desert id
kind: 'calm' | 'breeze' | 'gale' | 'storm'
start: { day: number; hour: number }
durationH: number
wind: Wind
/** When Tower announced it. A plan is only a plan once people can read it. */
publishedAt: { day: number; hour: number }
/** Tower changing its mind. The published event is kept; this is what happens instead. */
revisions: { at: { day: number; hour: number }; kind: WeatherEvent['kind']; wind: Wind; reason: string }[]
}
3.3 What changes from v1, and why
- Relief parameters move onto the feature. v1 had
kindon the place and anopwith its own parameter bag in a separate file. v2 has one discriminated union: the kind decides what gets generated, and the TypeScript type decides which parameters exist. - Canyons and ridges stop being kinds. A canyon is what a river valley is where
it passes through high ground; The Narrows becomes a named
Markeron the river's course. A ring ridge is aMountainsfeature with abandarea. - Sub-elements live inside their feature. Peaks inside mountains; stands and clearings inside a forest. That is what makes them move, hide and delete together.
- Private features vanish from readers' terrain too. A
public: falselake is stripped from the reader's file (D-011), so readers do not get its basin either. That is the correct behaviour — a hole in the ground shaped like an unpublished lake would leak it. Since D-072 the author uses the same app readers do, so an unpublished feature exists only incontent/world.jsonuntil it is published.
4. The concepts, one by one
Each section says: what is authored, how it is generated, how it is drawn, and which libraries (if any) it uses. Library choices are justified in §7.
4.1 Mountains — "reasonable peaks"
The problem with today's approach. A dome with ridged noise laid over it, normalised to the declared height. It gives one summit at an arbitrary spot in a blister of noise, and no real ridges, saddles or valleys.
As built (D-065), stage 3 is not an iterative erosion but the stream-power steady state, computed directly over the drainage network and scaled back to the authored massif; overlapping ranges erode as one group; and authored tarns keep a shelf. The description below is kept as the plan; §15 says why each changed.
Generation, in three stages.
- A ridge graph. The authored peaks plus
generatedPeaksextra summits (Poisson-disk placed, heights falling off from the spine and from the authored peaks) are triangulated with delaunator. The minimum spanning tree of that triangulation, weighted by the height drop along each edge, gives the main ridges; a fraction of the remaining edges become spurs. Each ridge edge gets a saddle height below the lower of its two peaks. - An uplift field. Elevation is a function of distance to the ridge graph:
a sharp crest at the ridge (the arête), steepening flanks, and a foot that blends
to the ground at the edge of
area. Authored peaks are pinned — the field is exactlyelevationMat each one. - Fluvial erosion. The uplift field is sampled on a feature-local grid
(120 m; 200 m for a ring range, since a band is 314 km long) and run through the
stream-power law with the implicit solver of Braun & Willett (2013), for a
fixed number of iterations proportional to
erosion. This is what cuts dendritic valleys and makes the ridges between them read as ridges. Authored peak cells are held fixed during it. The result is cached (§5.4).
Below the grid, detail comes from derivative-damped fBm (noise whose higher octaves are suppressed on steep slopes — the standard "eroded noise" trick), evaluated per tile.
Drawn as: terrain tiles (§6). Material by elevation and slope: vegetation below
treelineM, scree and rock above it, snow above snowlineM only where the slope is
shallow enough to hold it (today's rule, kept).
Test: every authored peak's generated elevation is within 1 m of elevationM,
and is a local maximum within 500 m.
4.2 Lakes — "waves"
Generation. The shore polygon is carved into a basin whose depth profile follows
distance from the shore. levelM: 'auto' fills to the lowest point of the rim, the
way today's tarn op fills from the floor up.
The water surface is real geometry. Level water in Tower is a surface of
constant radius (§1.9.3), so a lake surface is a patch of a cylinder at
R − levelM. That makes "the far shore rides 2.3 km above your tangent plane" true
in the render for free.
Waves are sized from the lake, not chosen. Wind waves are limited by fetch, the distance the wind blows over water. The JONSWAP fetch-limited relations give:
- significant height
Hs ≈ 0.0016 · U · √(F / g) - peak period
Tp ≈ 0.286 · (U / g) · (g F / U²)^⅓
A 10 m/s wind across a 30 km lake gives 0.9 m waves at a 4.2 s period. A mountain tarn 2 km across gives 0.2 m. The shader is a sum of 4–6 Gerstner waves fitted to that height and period near the camera, fading to scrolling normal detail at distance. Shore foam where the water is shallow, from the depth between the water surface and the terrain under it.
4.3 Rivers — "flow"
Canon first. A Tower river exists only because the floor was shaped to make it descend (§1.9.3), and it runs in a closed pumped or weather-driven loop (§1.9.5). So the generator's one hard rule is: the water surface descends monotonically from source to outlet. If the terrain along the drawn course would make the river flow uphill, the valley is cut deeper until it does not — and the compile reports how much it had to cut, because that is information the author wants.
Generation.
- The course is smoothed (Catmull–Rom, as today) and resampled every 200 m.
- Long profile. Changed in Phase 1 (§15). Not a concave curve between the ends, and not clamped to the ground: the least-earthwork descending profile — the weighted isotonic regression of the ground along the course, held to the outlet's level at the mouth and a lake's level at a source in one, falling at least 2 m/km so no reach is a canal. It cuts where the ground is high and builds the ground up where it is low, in whatever balance moves the least earth.
- Hydraulic geometry. Channel width ≈
a · Q^0.5and depth ≈c · Q^0.4(Leopold's relations), so flow in m³/s sets the size. A tributary adds its flow to the river it joins, downstream of the confluence. The Narrows gets its 392 m³/s from the four rivers that feed it, not from a number typed in. - Valley carving, and embankments. Distance from the centreline carves a
channel, banks and a valley whose width depends on the ground: narrow where the
river crosses high ground (canyon), wide where it is low (floodplain). Where the
ground is below the profile, the same distance builds an embankment with 1:6.7
flanks. This replaces v1's hand-authored
reaches.
Drawn as: a ribbon mesh along the centreline, at the profile's water level,
with UVs running along the flow. Flow is a normal map scrolled along the ribbon at a
speed from Manning's equation, the technique of Three's Water2 flow-map example.
Where the profile is steep enough, rapids (white, faster); where a drop exceeds ~20 m,
a waterfall sheet that leans antispinward by the impact angle
canon/motion.ts already computes — 11° for a 1 km fall. The waterfall is the
compass (§1.9.2), and this would be the first time the render shows it.
4.4 Cap seas — "waves and sailing ships"
Generation. A band carved into a basin, as today's flood op, with a water surface at constant radius across the whole ring.
A ring sea has unlimited fetch. Wind blowing around the circumference crosses the
same water forever, so the fetch limit of §4.2 never applies and the sea reaches a
fully developed state: Hs ≈ 0.21 · U² / g, which is 2.1 m at 10 m/s and
8.6 m at 20 m/s. No lake on Earth does that; only open ocean does — and in Tower
it happens only when Tower schedules it (§4.9). Wind speed at any moment comes from the
weather plan, and the sea state follows it with a lag of a few hours, so a storm
builds and a sea goes on running after the wind drops. Waves are Gerstner as in §4.2,
with longer wavelengths and a swell component.
Ships. count ships per sea, each with a route: ring circles the world
along a constant z; coastal follows the shore; crossing shuttles between named
harbours. Position is a pure function of route and clock time, so every reader
at the same moment sees the same fleet in the same place. Ships read the
published plan: a ship does not sail into a published storm, and waits in harbour
until it has passed. Each ship samples the same wave function on the CPU at its hull to
heave, pitch and roll.
Hulls and sails are built procedurally (a lofted hull, a mast, one to three sails that are planes with a billow), a few hundred triangles each, instanced. Sails set to the sea's wind, so a ship on a ring route sailing against the wind tacks.
As built (§18): ring routes only, no harbours drawn, flat sails, and no tacking yet.
Scale check. A 40 m ship subtends 0.9 arc-minutes at 150 km, the edge of the haze horizon. At a 60° field on a 1,500 px-wide view one pixel is 2.4 arc-minutes, so it is under half a pixel: invisible to the naked eye there, a speck through the binoculars (the field-of-view control). At 20 km it is 7 arc-minutes, three pixels — a readable sail.
4.5 Desert — "hills, plateaus, visible past water"
Generation, as layers inside the desert area:
- Dunes. Anisotropic ridged noise stretched perpendicular to the wind, with an asymmetric profile (gentle windward slope, steep slip face). Tower makes its own wind (§1.9.4), so the dune direction is a statement about what Tower does there.
- Rocky hills. Low domes of ridged noise,
hills.coverageof the area. - Plateaus, mesas and buttes. A noise mask thresholded into plateau shapes, then
raised by
heightMthrough a terrace function withterracessteps: flat tops, near-vertical cliff bands, talus slopes below. Small isolated parts of the mask become buttes. The rock shader paints strata by elevation, so the layers line up across separate mesas, which is what makes them read as one eroded formation. - Past water — quoted, not remembered (§0.1). Three marks, each switchable:
- Dry washes — a drainage network generated by the same flow routing as the mountains (§4.1 step 3), cut shallow into the plateaus and the plain, with a pale gravel floor and no water. Tower routes them as if water had carved them, because that is what the originals it copied look like.
- Salt pans — the lowest closed basins, flattened and painted white.
- Abandoned shorelines — horizontal benches at fixed levels around a salt pan, the bathtub rings of a lake that never existed.
Because none of it is history, the generator is free to place these as set pieces — a salt pan ringed by shorelines where the author wants the view, not where a drainage model would demand one.
As built (§20): all of the above, with the pans placed in the lowest ground rather than by hand, and the terraces cut by the tiles rather than compiled.
4.6 Forest — "trees, planned concentrations, meadows, paths"
Trees are real size. Conifers 20–45 m tall with narrow crowns; broadleaf 15–30 m
with crowns ~0.3 of their height. Size varies with ageYears per stand and with
elevation (smaller toward the treeline).
Where trees go is a density field, in stems per hectare, built from:
- the forest's
densityPerHa; - stands — authored polygons with their own density, species mix and age.
These are the planned concentrations: Tower plants its forests, and a stand can
be
natural(irregular) orrows(a plantation grid); - meadows — authored clearings plus a generated fraction from a thresholded noise field, with soft ragged edges;
- exclusions — water, roads, paths, buildings, slopes too steep, above treeline.
Placement is stratified, not a whole-domain Poisson solve. The surface is divided
into a fixed global grid of cells sized to the tree spacing; each cell hashes
(seed, cell) to decide whether it holds a tree and where inside it, accepting with
probability density / max density. This is blue-noise-like, and — the property
that matters — any tile can compute its own trees without knowing any other
tile. A sequential Poisson-disk algorithm cannot promise that (§7.3).
Paths. A trail network connecting meadows, stands and the forest edge, routed by A* over a slope-cost grid and smoothed (§4.8). Paths clear trees and flatten a 2 m corridor.
Drawn as three distances:
| Distance | Representation |
|---|---|
| < 400 m | instanced meshes: trunk plus layered cones (conifer) or clustered lumps (broadleaf), 30–80 triangles |
| 400 m – 4 km | instanced camera-facing impostors, one quad per tree |
| > 4 km | no trees. The terrain shader paints canopy, and tile vertices in the forest are lifted by the local mean canopy height with a rough top, so a forest has a silhouette and an edge from far away |
The tall forest. A forest with giants: true grows sequoia-class trees at
90–115 m — coast redwoods reach 116 m on Earth, and hydraulics cap any tree near
122–130 m (Koch et al., 2004), so Tower is growing them close to the limit of what a
tree can be at 1 g. Stems are far sparser (≈ 40–80 per ha) and trunks 5–8 m across.
Honest about what that looks like: the ordinary canopy is ~30 m, so the giants stand
~80 m proud of it as a dark wall with a ragged top. That is 0.46° at 10 km —
about a thumb's width at arm's length, clearly a landmark — and 0.09° at 50 km, a
line on the horizon. Devils Tower is 265 m, so a giant forest on level ground is a
third of that. If the author ever wants it to read at Devils Tower scale, the forest
can stand on a raised plinth of ground (a Mountains feature of low ruggedness), and
the two heights add.
Where it goes: near the capital, placed at random — once (author, 2026-09-16). The
port script creates it as its own Forest feature, The Tall Wood (provisional
name), about 50 km² with a ragged outline. Its position is drawn from the world seed:
on land, inside Greatwood, 15–50 km from the capital, and clear of The Deep, the
capital's own boundary and its roads. The result is written into the world file,
so it is placed once and then stays put for everyone; the author can move it like any
other feature. Where it overlaps Greatwood, the Tall Wood's trees replace Greatwood's
(the smaller forest wins inside its own area).
Why not render every tree. Greatwood is 216 × 314 km. At 450 stems/ha that is 3 billion trees. Only the ones within 4 km of the eye exist at any moment.
As built (§21): the density field, meadows, the three distances and the tall wood, with the ranges closer in than planned (meshes to 320 m, cards to 760 m, canopy skin beyond) and without authored stands or paths — stands and the trail network wait for phase 7's router.
4.7 Settlements — "blocky buildings and a few simple roads"
As built so far (D-070): a settlement is a point with an extent — a size and a radius, drawn on the map as a ring — and the Capital is 20 km across, centred on the axial midpoint, which reaches The Deep's shore. The layout inside it is this phase's work.
No usable city generator exists on npm (§7.3), so this is written in-house and kept
deliberately simple, as asked. One generator handles every size; size sets the
defaults:
| Size | Radius | Buildings | Streets |
|---|---|---|---|
| hamlet | 150 m | 5–20 | one lane |
| village | 400 m | 50–300 | a main street and a few lanes |
| town | 1.2 km | 500–3,000 | arterials plus a grid or organic core |
| station | 300 m | 20–80 | mountain settlements at the tarns (Sisters Station, Hollow Station); lifts and runs are a later addition |
| city | 6 km | 20,000+ | the capital |
Filling out the world. Beyond the authored places, the compile places generated
settlements at settlementDensity. Candidate sites are scored by suitability — flat
ground, fresh water within 2 km, not in the desert core, not above the treeline — and
accepted largest first with a minimum spacing that grows with size. Generated
settlements are named for their size — "Hamlet", "Village", "Town" — and marked
as generated on the map (author, 2026-09-16). Several share a name, which is correct:
nobody has named them yet. Adopting one — writing it into content/world.json as an
authored feature — pins it: after that, changing the world elsewhere cannot move or
remove it.
Layout, for every size:
- Arterials.
arterialsroads from the centre to the boundary, bent to avoid water and steep ground, and joined to any external routes that reach the city. - Local streets. A
grid(rotated to the main arterial),radial(rings plus spokes) ororganic(streets grown with branching and snapping) atblockMspacing, clipped to the boundary and to the shoreline. - Blocks are the faces of the planar road graph, inset by half the road width (clipper2-js offsetting).
- Lots by recursive split of each block along its longer axis until lots are 15–40 m wide, keeping only lots with street frontage.
- Buildings are the lot inset by a setback, extruded to
storeys × 3.2 m. Height rises toward the centre ifpeakAtCentre. Flat roofs. Per-building colour jitter. A city pad flattens the ground under the built area.
The Spindle (3.2 km) stays a tower marker with its own mesh, not a generated
building.
4.8 Roads and paths — "around the capitol and through the forest"
- Drawn routes are smoothed and draped on the terrain.
- Routed routes (
{ from, to }) run A* over a cost grid: cost rises steeply with slope, water is impassable except at a bridge cost, existing roads are cheap (so routes merge into networks rather than running in parallel). The result is simplified and smoothed. - Default network: every settlement is joined to its nearest larger neighbour by a road, and to the next nearest as well where that saves a long detour, so the network is a connected web rather than a star around the capital. Hamlets get tracks. Forests add paths (§4.6).
- Grading. A route cuts and fills a corridor to a smoothed longitudinal profile with a maximum grade (8% road, 20% path), blended into the ground over a shoulder. Surface material is paved or packed earth.
4.9 Weather plan — Tower's schedule
Every wind in Tower is Tower's doing (§1.9.4). Tower publishes its plans so that people can live around them — sail, harvest, travel — and changes them whenever it chooses: for example, cancelling a storm because someone foolish has gone sailing into it. Both halves are data here, because both are story.
- Baseline: each sea, lake and desert has a default wind when nothing is scheduled.
- Events: calm, breeze, gale or storm on a target, for a start and a duration, with the moment Tower published it.
- Revisions: Tower's changes of mind, each with the moment it happened and a reason. The published event is kept, so the app can show both what people were told and what Tower actually did.
The clock gains a day. Today the interior has a time of day only (D-053). Scheduling
needs a day number, so the clock becomes day + phase. Days count from 0, with no
week or month names (author, 2026-09-16). The panel shows the plan
as Tower would publish it: a board of upcoming events, with revisions struck through
and replaced.
What the weather drives: wave height and direction on seas and lakes (§4.2, §4.4), sail trim and harbour decisions for ships, and the dune direction in a desert, which uses the target's baseline wind. What it does not drive: clouds and rain, which are still not drawn (D-049).
Default plan. The port script writes a baseline breeze everywhere and a repeating pattern of a few scheduled storms per month on each cap sea, so the feature is visible before anyone authors an event.
As built (§18): the plan is written into content/world.json directly rather than by the
port script, and deserts do not read it yet (Phase 5).
5. Generation pipeline
5.1 Overview
flowchart LR
W[world file] --> C[world compile<br/>workers, on load + on edit]
C --> K[(feature caches<br/>ridges, erosion grids,<br/>river profiles, city layouts,<br/>route polylines)]
C --> P[coarse world field<br/>~500 m]
P --> M[map view preview]
P --> X[exterior x-ray + far tiles]
K --> T[tile generator<br/>worker pool, on demand]
T --> R[interior renderer<br/>terrain · water · trees · buildings · ships]
T --> G[ground query<br/>standing, collision]
5.2 The world compile
Runs in a worker on load and after every edit. It never produces pixels for the 3D view; it produces the expensive, whole-feature results that tiles then sample.
In dependency order:
- Validate and index. Parse, fill defaults, give every feature an influence box. Phase 1 checks the boxes directly: with 28 features that is cheaper than an index. flatbush comes in when settlements and routes make the count large.
- Landforms. Mountain ridge graphs and erosion grids; desert plateau masks and wash networks.
- Water. Lake levels (
autoneeds the landforms), river long profiles (needs outlet levels), river flow accumulation down tributaries. - Settlement. Generated settlement sites, then layouts for every settlement; route networks (need the final terrain cost grid).
- Weather. Resolve the plan's events and revisions into a wind timeline per target.
- Coarse world field. Elevation and surface material for the whole world at ~500 m (1080 × 628), by evaluating the full elevation function once per cell. This feeds the map, the exterior's x-ray and the most distant tiles.
Incremental. Each result is keyed by a hash of the inputs that produced it. Editing a forest's meadows reruns nothing in steps 2–3; moving a peak reruns that mountain range, the rivers whose course crosses it, and the routes whose cost grid changed. Tiles are invalidated only where the changed features' influence boxes overlap them.
5.3 The elevation function
One pure function, used by the tiles, the coarse field, the ground query and the tests:
elevation(θ, z) =
ground(θ, z) gentle base relief
⊕ landforms: mountains (max), desert (add) from their caches + detail noise
⊖ basins: seas, lakes carved
⊖ valleys: rivers, washes carved to their profiles
⊜ grading: city pads, roads, paths cut and fill toward a target
Features are fetched from the spatial index, so a point in open plain evaluates the ground and nothing else.
Noise is evaluated on the cylinder itself. 3D simplex noise at
(R cos θ, R sin θ, z) / scale is seamless around the circumference by construction.
That retires D-019's snapped-lattice workaround, which was a fix for sampling 2D noise
on a surface that wraps.
A sibling function, surface(θ, z), returns material weights (rock, scree, snow,
grass, meadow, forest floor, sand, gravel, salt, soil, paving) and the scatter
densities (trees per ha, whether a building lot is here).
5.4 Caching
- Memory: an LRU of generated tiles (~400 tiles on desktop, ~120 on phones).
- IndexedDB: feature caches keyed by input hash — erosion grids above all, since they are the slow part (§8.3). A returning reader skips the compile's heavy steps.
- No cache is authoritative. Clearing site data costs time, never correctness.
5.5 Determinism, and the seed
The seed is part of the world file, so it is the same for everyone. Nothing is rolled per visitor, per device or per page load. Two readers standing on the same spot see the same trees, the same village, and the same ship on the same wave; a reader's link lands on what the author saw.
Where the numbers come from:
| Seed | Set when | Changes when |
|---|---|---|
world.seed |
once, by the port script, to a fixed value | only if the author deliberately rerolls the whole world |
feature.seed |
once, when the feature is written into the file — the one moment a random number is drawn — and saved with it | only if the author asks for that feature to be rerolled |
| everything generated | never stored; recomputed from the two seeds above and the feature's parameters | whenever those change |
content/world.json carries the seeds, and the world.json the app loads is made from
it with the same seeds, so what the author sees is exactly what readers get.
Placements made "at random" — generated peaks, generated settlements, the Tall Wood's
position — are random in the sense of chosen by the seed, not different each time.
- One integer hash (the existing
Math.imulhash fromtools/noise.mjs, kept) and a seeded PRNG built on it. NoMath.randomanywhere in generation. - Every feature's randomness comes from
hash(world.seed, feature.seed), wherefeature.seedis a number written once when the feature is created. It is not derived from the id: ids follow names until pinned (D-012), so an id-based seed would reshuffle a forest every time it was renamed. - A test generates a fixed set of tiles twice and compares them byte for byte.
6. Rendering in the interior view
6.1 Terrain level of detail
The single 512 × 768 cylinder is replaced by a quadtree of tiles in the unrolled (z, arc) plane, bent onto the cylinder.
- Level 0: 16 tiles along × 10 around — 33.75 × 31.4 km each.
- Each level halves the tile. Level 10, the finest, is 33 × 31 m.
- Every tile is a 65 × 65 vertex grid (33 × 33 on phones), so level 0 has 527 m vertex spacing and level 10 has 0.5 m.
- A tile splits when the eye is closer than 1.8 tile-widths (1.6 on phones), measured to a sphere around the tile's actual relief, and is drawn from its parent until all four children are ready — nothing pops out, detail pops in. Measured: about 60 tiles a level and 740 chosen, of which frustum culling leaves about 200 draw calls and 1.9 M triangles on a laptop.
- Neighbouring tiles at different levels are sealed with skirts (a short curtain hung from each edge). Geomorphing is a later refinement if popping is noticeable.
- Around θ the tree wraps: tile index 9 neighbours tile index 0.
Precision. Vertices are bent onto the cylinder in the worker, in coordinates relative to the tile's own centre, and the tile mesh is positioned at that centre. Float32 at 50 km in kilometre units is ~6 mm; relative to a tile centre it is sub-millimetre. Doing the bend in the vertex shader from absolute coordinates would put 6 mm of jitter on every tree and roof edge. The logarithmic depth buffer stays.
Standing on the ground calls the same elevation(θ, z) function, so D-035's
problem — the CPU and GPU disagreeing about where the ground is — disappears rather
than being worked around.
6.2 Surface shading without a bitmap
The colour raster is gone. Each tile vertex carries material weights from
surface(θ, z); the fragment shader blends a small palette of procedural materials
(colour, noise-driven variation, slope darkening) — splatting, with the "texture"
computed rather than loaded. Distant tiles average to the same colours, so there is
no seam between near and far.
Every new material — terrain, water, trees, buildings, ships — includes
HAZE_GLSL, SUN_GLSL and the shared uniforms (uExposure, uSun …), so haze, the
sun and night apply to all of them uniformly (D-074). This is a hard rule: a material
without it will glow through 200 km of air.
6.3 Water, scatter, ships
- Water bodies: each lake and sea has its own tile set at constant radius, drawn only where the terrain is below the level (the depth test does the shoreline).
- Rivers: ribbon meshes per tile.
- Trees, buildings: one
InstancedMeshper tile per model; tile visibility is the culling. Upgrade path:@three.ez/instanced-meshif per-instance culling is needed. - Ships: one instanced mesh per sea, updated per frame from the clock.
6.4 Flying
The interior already has eye height to 25 km and the drag-the-ground gesture (D-059). Added:
- Keyboard flight: W/S forward/back, A/D strafe (arrows too), Q/E down/up, shift to boost; speed is 1.2 × height above ground per second, at least 2 m/s, and shift multiplies it by five. Climbing is a fixed factor per second — ×2 a second, ×7.4 with shift — since the same ×5 there would take 100 m to 40 km in a single second.
- Terrain clearance: the eye never goes below ground + 1.7 m.
- Scenic tour (§19): a scene every thirty seconds, flown by the view itself.
- Eye height ceiling raised toward the axis (~45 km; since D-084, the beam's edge at 48.4 km) so the whole world can be seen from the middle of the air, if wanted.
7. Libraries
7.1 Adopt
| Package | Version | License | Use | Why this one |
|---|---|---|---|---|
| simplex-noise ✅ Phase 1 | 4.0.3 | MIT | 2D/3D simplex for every field | TypeScript, tree-shakable, takes a seeded random function; 3D is what makes noise on the cylinder seamless |
| delaunator ✅ Phase 2 | 5.1.0 | ISC | mountain ridge graph; city road graph faces | the standard, fast, tiny; updated 2026 |
| flatbush | 4.6.2 | ISC | spatial index of feature influence boxes | static packed R-tree, ideal for "which features touch this tile"; transferable to workers as one buffer |
| clipper2-js | 1.2.4 | Boost | polygon offset (road insets, setbacks), clipping (blocks to boundary and shore) | offsetting is the operation needed, and polygon-clipping does not offset. Check its bundle cost before adopting; the package ships 1.8 MB unpacked, mostly sources |
| comlink ✅ Phase 1 | 4.4.2 | Apache-2.0 | worker RPC | removes the message-passing boilerplate for the worker pool; 4 KB |
Three.js itself supplies ShapeUtils triangulation (earcut) and the Water2 flow-map
technique, so neither needs a separate package.
7.2 Considered, useful later
| Package | Why not now |
|---|---|
@three.ez/instanced-mesh 0.3.16 (MIT) |
per-instance culling and LOD; tile-level culling is enough to start |
three-mesh-bvh 0.9.15 (MIT) |
fast raycasting; only needed if picking against generated meshes |
@thi.ng/poisson (Apache-2.0) |
Poisson sampling with density functions — but sequential over a whole domain, which breaks per-tile independence (§4.6). Fine for placing generated peaks in the compile |
7.3 Looked for and not found, or not suitable
- City / town layout. Nothing on npm. Watabou's Medieval Fantasy City Generator is
Haxe and GPL-3.0.
garden-cityon GitHub is MIT and structurally close (road graph → blocks → buildings) but unpublished, 0 stars. Write our own (§4.7), borrowing ideas only. - Erosion. No npm package. The GitHub projects found are demos (WebGPU, particle erosion) rather than libraries. Braun–Willett stream power is ~200 lines and deterministic, which a droplet simulation is not. Write our own.
- Terrain LOD.
@interverse/three-terrain-lod(MIT, 2026) is a quadtree for a flat plane with a single heightmap;three.terrain.jsgenerates small heightmap terrains. Neither handles a cylinder, per-tile worker generation, or a surface that wraps.@mapbox/martinibuilds adaptive meshes but assumes a square 2ᵏ+1 grid per tile with no crack handling between LODs. Write our own quadtree (~500 lines). - Road routing.
ngraph.pathandpathfindingneed an explicit graph or grid object; for a million-cell cost grid a typed-array A* with a binary heap in a worker is smaller and faster. Write our own. - Ocean. Tidewater and Three.js Water Pro exist, but target WebGPU/TSL or are commercial, and none knows a water surface is a cylinder. Gerstner waves in our own shader, including the haze.
8. Performance budgets
8.1 Frame (desktop, 60 fps target)
| Budget | |
|---|---|
| terrain tiles drawn | ≤ 250 (≈ 1.05 M vertices at 65²) |
| trees, meshes | ≤ 60 k instances within 400 m |
| trees, impostors | ≤ 400 k within 4 km |
| buildings | ≤ 50 k in view |
| draw calls | ≤ 600 |
8.2 Generation
| Budget | |
|---|---|
| one tile, all layers | ≤ 60 ms in a worker |
| workers | hardwareConcurrency − 1, capped at 6 |
| first usable view after load | ≤ 3 s (coarse tiles), full detail near the eye ≤ 8 s |
| edit → visible change | ≤ 2 s for anything except a mountain range's erosion |
8.3 The expensive one
Erosion on the Girdle's ring at 200 m is 425 × 1571 ≈ 670 k cells; Braun–Willett at ~100 iterations is estimated at 3–8 s in a worker. When it is paid, exactly:
| Situation | Wait |
|---|---|
| first ever visit (nothing cached) | the full compile, 3–8 s on a desktop, behind the status dialog (§10.1) |
| returning visit, world unchanged | well under 1 s — the caches in IndexedDB are reused |
| editing a mountain range (peaks, area, erosion) | that range's erosion again, 3–8 s for the Girdle, less for smaller ranges |
| any other edit | ≤ 2 s; no erosion reruns |
The author accepted this (2026-09-16), so precomputed erosion grids are not shipped.
8.4 Phones
Rule: a phone sees the same world, not a thinner one. It sees less of it at once.
The forest's density, its stands and meadows, where every tree stands, every building and every ship are the same on a phone as on a desktop — they are authored canon, and a reader holding a phone in a meadow should be standing in the same meadow as the author. What shrinks is reach and cost:
| Desktop | Phone | |
|---|---|---|
| tile grid | 65² | 33² |
| trees as meshes | 400 m | 150 m |
| trees as impostors | 4 km | 1.5 km |
| canopy beyond that | yes | yes, same shading, so the forest's outline is unchanged |
| workers | up to 6 | 2 |
| ship wave sampling | all visible ships | within 5 km |
So yes, a phone flies through a full-density forest; the individual trees simply start closer in. The tier is chosen automatically from the GPU and memory, with a Detail: auto / low / high override in the panel. A first compile on a phone is estimated at 2–3× the desktop time, 10–20 s, which the status dialog carries.
9. Views other than the interior
- Map. Draws a hillshaded, material-coloured image of the coarse world field, produced live by the compile, with feature geometry drawn over it as vectors. It is still an image on screen, but it is a view generated from the features, not a painted source of truth. The vectors are changed in the world file.
- Exterior. The x-ray reading uses the same coarse field as its texture. Looking in through the windows (D-060) uses only the coarsest tiles, since nothing finer is resolvable through 100 km of glass and haze.
10. Editing
Rewritten for D-072. The world is edited in content/world.json, not in the
browser: the author describes a change, Claude makes it in the file, and the app
regenerates from it. There is no writer mode, no local draft and no export. With
npm run dev running, saving the file reloads the open page; the published site
changes when the commit is pushed. The app loads world.json, which vite.config.ts
makes from the file with every notes emptied and unpublished features left out
(design.md §3.1).
What each concept is authored as — the parameters a change touches:
| Concept | Shape | Parameters |
|---|---|---|
| Mountains | polygon or band | peaks (name, position, elevation), generated-summit height and count, ruggedness, erosion, snowline |
| Lake | polygon (the shore) | level (auto or a number), depth, basin, shore, rim, wind |
| River | polyline, source to mouth | flow, source, outlet (by id, or auto) |
| Sea | band or polygon | level, depth, shore, wind, ships, ice (the ice edge and where the fast ice begins) |
| Desert | polygon or band | uplift, edge, dunes, hills, plateaus, past water |
| Forest | polygon or band | uplift, edge, density, conifer share, meadows, treeline, giants |
| Farmland | polygon or band | edge (D-095); the fields are laid out from where they are |
| Settlement | point | size, radius; a generated one is adopted by writing it in |
| Weather | the plan's events | target, kind, start, duration, wind, when published, revisions with reasons; the cycle |
| Road / path | polyline | width |
Each change goes: file → compile (only what the change touches, §5.2) → map and interior regenerate. A change that does not touch the ground — a name, a blurb, a note, a wind — regenerates nothing.
10.1 The status dialog
Whenever a compile runs longer than 400 ms — always on a first visit, and after editing a mountain range — a dialog covers the view with a progress bar and one line saying what is happening, in the manner of Terraria's world creation. Shorter edits show only the small indicator.
Every message is a real step. A line appears when that step of §5.2 starts, and it names the actual feature, so the dialog is honest progress rather than decoration. The voice is Tower's, since Tower is the one making all of it (§0.1). The author approved these lines (2026-09-16):
| Step | Message |
|---|---|
| validate | Reading the plans |
| mountain uplift | Raising {Snowcrown} |
| erosion | Letting rain find its way down {the Girdle} |
| desert | Quoting the deserts of Earth in {the Dry Reach} |
| desert washes | Cutting riverbeds for rivers that never ran |
| lakes, seas | Filling {The Deep} |
| river profiles | Teaching {The Long Water} which way is down |
| forest | Planting {Greatwood} |
| giants | Letting {the tall wood} grow as tall as a tree can |
| settlements | Choosing where people will live |
| city | Laying out the streets of {the Capital} |
| routes | Walking the roads in |
| weather | Publishing the weather |
| coarse field | Drawing the map |
Steps that take less than a few frames still show their line briefly, in order, so the sequence reads as the world being built up rather than as a flicker.
11. Existing decisions this affects
These need an explicit yes from the author before building, since some of them were deliberate choices:
| Decision | Effect |
|---|---|
| D-006 Interior fidelity: simple ground, "no scenery meshes, no settlements, no vegetation" | Reversed. Trees, buildings and ships are exactly that. design.md §4 also lists "procedural scenery, settlements" as explicitly out of scope |
D-019 Geography authored as data, generated by npm run terrain, with no dependencies |
Superseded. Still data-first, but generated live, and five runtime dependencies are added |
| D-020, D-051 (the world's belts, the Girdle) | Kept as content, ported into v2 features |
| D-021 (terrain tool progress) | Retired with the tool; progress moves to the UI |
| D-024, D-047 (x-ray raster on the hull) | Kept, fed by the live coarse field |
| D-035 (CPU height readback) | Retired: the CPU calls the elevation function directly |
| D-039, D-057 (raster height range, no mipmaps) | Retired with the rasters |
design.md §2.4 "water has no surface" |
Reversed: water is geometry |
| D-049 (rain not drawn) | Kept. Wind and waves become scheduled weather (§4.9); clouds and rain stay undrawn |
| D-053 (the day is a front) | Kept, and the clock gains a day number for the weather plan |
design.md §1.11 "The cap seas are not designed" |
Superseded by §0.1. The seas pool where gravity puts them, but every landform around them is now a choice Tower made |
test/raster.test.ts |
Deleted; replaced by generation tests (§13) |
12. Author's answers, and what is still open
12.1 Answered (2026-09-16)
| # | Question | Answer | Where it landed |
|---|---|---|---|
| 1 | "Visible past water" | Tower added these forms because it saw them on Earth and other planets and thought them appropriate — not because water was ever there | §0.1, §4.5 |
| 2 | Ships and wind | Tower is in charge of the weather, publishes its plans so people can plan, and changes them when it wants — e.g. for a foolish sailor in a storm | §4.4, §4.9 |
| 3 | Ring-sea storms | Up to Tower; it schedules them | §4.4, §4.9 |
| 4 | Settlements | Generate them to fill out the world | §4.7 |
| 5 | Tree scale | Real-world sizes, plus a tall forest of giants that reads as a landmark | §4.6 |
| 6 | Compile wait | 3–8 s is acceptable behind a working dialog; Terraria-style status messages welcome | §8.3, §10.1 |
| 7 | Phones | Left to the design: same world at full density, shorter draw distances | §8.4 |
12.2 Answered (2026-09-16, second round)
| # | Question | Answer | Where it landed |
|---|---|---|---|
| 1 | Tower's calendar | Days count from 0, no names | §4.9 |
| 1a | Is the seed the same for everyone? | Yes — it lives in the world file (the author's question; this was already the design, now stated explicitly) | §5.5 |
| 2 | Generated settlement names | Named for their size: "Hamlet", "Village" | §4.7 |
| 3 | The tall forest | Placed at random near the capital, once, then fixed | §4.6 |
| 4 | Status dialog lines | Approved as drafted | §10.1 |
Nothing is open. The design is ready for Phase 1.
13. Testing
Generation is pure TypeScript and runs under vitest without a browser:
- Determinism: fixed tiles generated twice are byte-identical, and a world compiled in two separate workers from the same file produces identical caches.
- Seams: a tile's edge heights equal its neighbour's, at the same and at different levels, including across θ = 0.
- Mountains: authored peaks within 1 m of their elevation and locally highest.
- Rivers: water surface monotone non-increasing from source to outlet; ends at its outlet's level.
- Water: lake surfaces at constant radius; no lake level above its rim.
- Forest: no tree in water, on a path, in a clearing, or above the treeline.
- Settlements: no building overlaps a road or another building, or leaves the boundary.
- Settlements: generated sites respect minimum spacing; every settlement is reachable from the capital by road.
- Weather: a revision overrides its event from its moment onward and not before; no ship is at sea inside a published storm.
- Routes: grade never exceeds the maximum after grading.
- Budgets: one tile of each kind generates under its time budget on CI (loose).
- Screenshots (D-042): the tour gains views of each concept from the ground and from altitude.
14. Phases
Each phase ends with something visible and runnable, documentation current, and a screenshot check before any commit.
| # | Phase | Delivers |
|---|---|---|
| 1 ✅ | Foundation | schema v2 + port script; status dialog; elevation function with ground + today's landforms reimplemented; worker pool; quadtree tile renderer on the cylinder; surface splatting; ground query; keyboard flight; map preview from the coarse field. Delete tools/terrain.mjs, the PNGs, raster.ts. Parity checkpoint: today's world, without the bitmap |
| 2 ✅ | Mountains | ridge graph, erosion, peak editing |
| 3 ✅ | Water | seas and lakes with surfaces and waves; rivers with profiles, flow and waterfalls |
| 4 ✅ | Weather and ships | weather plan with revisions and the day clock; plan board in the panel; procedural hulls, routes, harbour waits, heave on the waves |
| 5 ✅ | Desert | dunes, hills, terraced plateaus, washes, salt pans, shorelines |
| 6 ✅ | Forest | density field, stands, meadows, the tall forest, three-distance tree rendering |
| 7 | Settlements, roads, paths | one layout generator for every size, generated settlements and Adopt, routing, grading, buildings |
| 8 | Editing polish, performance, phones | incremental compile everywhere, IndexedDB caches, phone tier, screenshot tour |
Phase 1 is the largest and riskiest: it changes how every pixel of the ground is made. Nothing after it is started until its parity checkpoint looks right.
15. Phase 1, as built (D-063)
What the plan did not know until the world was generated honestly:
- The rivers cross a lowland they cannot climb out of. Between the Girdle's foot and the Dry Reach (z 115–135 km) v1's ground is about 60 m high, and the plateau beyond it 800–1,100 m. v1 painted its rivers across at ~1,000 m. The first profile — clamped to the ground — dropped every river to The Deep's 120 m and cut a canyon 900 m deep through the whole desert; the second — a graded curve — needed a 1,300 m embankment. The least-earthwork profile of §4.3 cuts The Narrows 300–500 m deep, which is what design.md §1.11 always said, and builds the lowland up by up to 880 m under each river. That is the world as authored, generated faithfully. Resolved by the author (D-064): raise the lowland. The Uplands, a region lifting the ground to ~1,000 m, bring the worst embankment under 200 m; and the profile now prefers cutting to filling (a 0.2-quantile isotonic fit rather than least squares), since a cut reads as a valley and an embankment as a dam.
- The hull was at the floor's radius. Seen from outside, generated sea floors and tile skirts came through it as a lattice. The skin now sits at the wall's outer face, a kilometre out (D-044, D-064), and nothing generated may reach it.
- Tiles were measured against the tallest mountain. A fixed 4.5 km relief allowance made every tile within 5 km of the eye split to the finest level: 11,000 tiles on screen. Each tile is now bounded by its own measured relief.
- Coarse tiles cannot show a river. A vertex of a 500 m tile landing in an 80 m channel painted a 500 m blue diamond, in a dotted line down every river on the far side of the world. Water now weighs by how much of a vertex's spacing it covers.
- The x-ray hull was mirrored end to end, before any of this. v1's hull UVs flipped the axis, so the Girdle's snow sat at the far cap. It shows in the committed v1 screenshot; fixed here.
- Per-tile memory. Normals travel as signed bytes, and every CPU copy of a tile is dropped once it is on the GPU.
16. Phase 2, as built (D-065)
Mountains are src/terrain/mountains.ts. What changed from §4.1 on contact with the
real ranges:
- Overlapping ranges are one group. The Girdle and the four massifs standing on it are summits on one ring, triangulated, joined and eroded together; the three desert ridges, which overlap each other, are a second group. Separately eroded, the massifs' valleys would have met the ring's at a seam.
- The ridges are blended, not maxed. A hard maximum of cone-like flanks meets its neighbour along a straight crease and the range reads as folded paper. The flanks are combined by a soft maximum (the fourth root of the sum of fourth powers) and their distances nudged by noise.
- Flanks are broad. A 4,300 m summit's roots reach 27 km and it still stands 2,000 m high 12 km out — v1's massif size, and where its tarns and ski stations were.
- Erosion is the steady state, not iterations. Braun–Willett iterations over a smooth uplift field cut almost nothing in a sensible number of passes, and what they did cut ran in parallel grooves along the grid. The stream-power steady state can be written down directly over a priority-flood drainage network — each cell above its receiver by the slope that keeps pace with uplift — and eight passes, each re-finding the network, give branching valleys and sharp divides (Cordonnier et al., 2016). Three things were needed to stop the grid showing through: a surface roughened at valley scales to start from, a fresh jitter on the flood each pass, and a channel threshold of 1 km² with a little hillslope diffusion, below which ground is slope rather than a one-cell gully.
- The steady state is scaled back to the author's shape. Its heights have their own
scale, so each cell is multiplied by the authored field over the steady state, both
blurred over 8 km. The Girdle stays 2,900 m and Snowcrown's massif 4,300 m; the ridges
and valleys inside them are the water's.
erosionblends that with the uncut field. - Summits are the top, exactly. Where anything within a few kilometres stands above a named summit the ground around it is scaled down smoothly; the summit is pinned to its elevation; a gentle cone trims the few cells pinning lifts past it; and it is pinned again. Fine detail is kept off the summit. Authored summits are exact to under a metre above the datum — the land under a summit is subtracted before it is pinned, where v1 added its 4,300 m on top of whatever ground was there — and higher than everything within half a kilometre (tested).
- Tarns keep a shelf. The steady state cut valleys through three of v1's four tarns. A cirque is ground the rivers did not cut down, so each authored bowl lake keeps its range at the uncut height around it. Sisters Tarn resolves to 2,173 m, beside the ski station design.md puts at 2,100 m.
- Editing. (The editor was removed by D-072; summits are now edited in the file.) Summits are listed in the editor with name and elevation, added by clicking the map and dragged to move; the range's generated-summit height, count, ruggedness, erosion and snowline are controls. Every change regenerates the range, and the service now runs one compile at a time, collapsing edits made during it into the next: a ten-step drag regenerated twice, not ten times.
- Cost. A compile is about 4 s in a browser worker for the whole world, nearly all of it the Girdle's erosion; tiles sample the 200 m relief grid and add detail at a few percent of the relief below it.
Parity, against the committed v1 screenshots: the map matches in layout, colour and the desert's ragged edge; standing at The Narrows and looking up from Greatwood match within a few metres of elevation. Cold load of the production build to a fully detailed interior is 4.3 s on the author's laptop; a compile runs in about 120 ms and the map draws in about 1.2 s.
17. Phase 3, as built (D-066)
- Water is a mesh per tile, on the tile's own grid: every wet vertex carries its water's level, a dry vertex beside one borrows it, and the depth test draws the shore. The ground under drawn water is bed. A river narrower than a tile's vertex spacing is not drawn as a surface there, only as a tint on the ground in proportion to how much of each cell it covers — a coarse tile's vertex landing in a channel drew a 500 m blue diamond, and a line of them down every river.
- Waves:
water.tsgives JONSWAP fetch-limited seas capped at Pierson–Moskowitz, and the tests pin §4.2 and §4.4's printed figures. Lakes and seas have awind; the editor shows the sea it raises. Phase 4 made that wind the baseline under Tower's weather plan (§18). - Rivers: speed by Manning, foam by grade. Ripples are advected downstream in the shader by a two-phase flow map.
- Waterfalls come from the profile. The least-earthwork fit pools reaches and steps between pools, so a step of 12 m or more is a fall. A river coming off a range stepped every 200 m for kilometres; consecutive steps are merged into one drop, with the reach below taking the foot's level — which cuts a gorge. The valley's walls are shaped from a separate wall level that ramps smoothly through the drop, so the gorge is a slot inside an unbroken valley; embankments use the same ramp, or they ended in a cliff the width of the embankment. Gorge walls stand at about 50°, wander with noise, and share the valley's curvature, or they cut a second cliff kilometres out.
- Sheets lean antispinward by
canon/motion.ts's free-fall drift, tested. - Skirts reach past a tile's steepest step. On a gorge wall a coarser neighbour's edge can be off by far more than 4% of the tile, and the cracks showed as dark panels.
- Rivers start and end at the water's edge. v1 drew the Long Water from the centre of Hollow Tarn, and kept whole its channel was cut across the lake floor, its water a stripe across the tarn (reported by the author). A river is now trimmed where it meets a lake — found by sampling the water, since a tarn's water fills only about the inner 70% of its outline, and a trim to the outline left the river stopping short of it.
- Narrow rivers stay visible at a distance. Where a tile is too coarse to draw a river as a surface it tints the ground by the square root of the share of each cell the river covers: by the plain share, a 40 m river in a 200 m cell was a 20% tint, and vanished where tiles coarsened.
- The beam is drawn after water, or the far side's seas blended over it and it went fuzzy where it crossed them. Reported by the author.
- The compiled world is cached in IndexedDB (§5.4), keyed by the world's terrain signature and the worker bundle's URL — hashed in a build, so a new build never reads an old world. The screenshot tour compiles once and gives every shot the warm cache, and caps each page at two workers: it had been compiling the world fifteen times, four at a time, with a full pool each, and the author's laptop ran hot for it.
- Cost: 99–112 fps with water in view, 2.1–2.6 M triangles, on the author's laptop.
18. Phase 4, as built (D-067)
- The clock has a day. The interior's clock is
day + time of day, from day 0; the panel has a Day field, running the clock turns the calendar over at midnight, and the day and time are remembered across reloads. World time in hours is the one input the weather and the ships need. - The weather plan is data in the world file (
weather: { cycleDays, events }), andworld/weather.tsreads it as pure functions of the plan and the hour: - What Tower does (
actualAt): the event in force, as last revised so far — a revision counts from the moment it was made, and only within the occurrence it was made in. The wind rises over the event's first hour and falls over its last. - What people were told (
inHarbour): the event as published, revisions ignored. Ships stay in harbour from three hours before a published gale or storm until it ends — including a storm Tower has quietly called off. That is the point of keeping both. A blow published with less than three hours' warning catches the fleet at sea, and it rides the weather out (changed in D-068; the default plan has one such squall). - A repeating plan:
cycleDaysrepeats every event (28 in the default plan), so the seas stay eventful without authoring a year of storms. - The sea lags the wind (
seaWindAt): waves answer an exponentially weighted memory of the last six hours of wind (2 h e-folding), so a storm builds and a sea runs on after it. - Waves are no longer compiled. Phase 3 baked each body's sea state into the compiled
world, which would have meant recompiling for every gust. Now a water vertex carries only
its body's index,
terrain/sea-state.tscomputes every body's waves from the weather a few times a second, and the shader reads them from a small table. Wind and ship count are out of the terrain signature, so editing them no longer regenerates anything. - Whitecaps cover the share of the sea Monahan and O'Muircheartaigh's fit gives for
the wind,
3.84e-6 · U^3.41— about 2% at 9 m/s, 9% at 20, 29% at 28. They break on two new short components (the wind's own sea on top of the swell); keyed to the whole sum they lined up along a storm's 600 m swell crests and left the troughs bare. - A precision fix, found through the storm. The water shader measured position around
the ring as
atan(y, x) × 50 km, which in float32 is good to about 6 m: anything finer — whitecap noise, short waves, river ripples — smeared into streaks radiating from the eye. Surface coordinates are now measured from a reference point near the eye that moves in whole 2,457.6 m steps (256 cells of every noise scale used), and each wave adds back its phase at the reference, computed in float64. Distant long waves also fade out earlier (from 16 pixels of wavelength rather than 8), which removes a moiré of stripes a storm swell drew near the horizon. - Ships (
terrain/ships.ts): each ring sea hasshipslanes (default 90), seeded from the world and the sea. A ship's position is a pure function of the hour: a steady 3–6 m/s round the ring, weaving gently in z, kept 4 km off either shore. Lengths run 8–45 m, mostly small. The hull, mainsail, jib and mast are procedural and instanced — three draw calls for every ship in the world; ships are drawn within 40 km. Each ship rides the same wave sum the shader draws (a TypeScript port, tested against its own derivative), heaving and pitching less the longer it is against the waves. The mainsail swings out running before the wind and in toward the centreline heading into it. On a sea with ice the lanes keep to the open water, 2 km short of the ice edge (D-091). - Sea ice (
terrain/ice.ts, D-090, D-091): a sea'sicegives its ice edge and where its fast ice begins, as z; the fast ice runs on from there to the sea's end. Each water sample carries its ice cover, 0..1 — the pack from 0.3 at its edge to 0.95 at the fast ice, 1 on the fast ice — with the edge wandering 1.5 km and the fast ice's 400 m, by noise. The water shader breaks the pack into 110 m Voronoi floes, each ice or open by its own hash against the cover, with leads between and cracks where two floes meet; the fast ice is one sheet cracked in 660 m cells. Ice stills the waves under it. The map whitens the water by the cover. - Farmland (
terrain/farms.ts, D-095, D-097). A farm given as a point grows at compile time over a 200 m lattice: from its seed, always taking the frontier patch with the lowest key — half its distance from the seed over the farm's radius, half a hash — and never a patch that is wet, sand, rock, a range (relief over 150 m), ice, or steeper than 12%, until it has itsareaKm2. The fields are a Voronoi of jittered blocks ~1.5 km across (a whole number round the ring) with 6 m tracks on their edges; in each block, a second Voronoi in the block's own frame — its own angle, field size 220–440 m and stretch 1.3–2.2 — measured from the block's centre so float32 holds; a third of those fields are cut into 2–4 strips. Each field takes a crop from a weighted table by hash; its rows run along its length. A field belongs to a grown farm if the patch under the point it was laid out from does. The same layout is written as GLSL, so the map and the views agree field by field. A sample carries only how much farmland it is, which clears forest; the ground shader draws the crop — rows over soil, bloom among the leaves, averaged once a row is under a pixel — and lights each field on its crop's own day from a uniform carrying the hours since sunrise. - Ice mountains (
field.ts glacier, D-093): a sea'sice.rangeraises rock from the sea bed atfromMtoheightMat the sea's end, steepening toward it (height ∝ t^1.6), ±30% round the ring and ridged by up to 0.7 of its height. Ice mantles it from where it leaves the water, as an ice sheet's plastic profile (τ = 150 kPa) to 350 m, draping 60% of the ridges; where a ridge stands out of the ice it is bare rock. Grounded ice is white however steep. The far cap's shader buries the glass's foot in ice to the height the ground stands at the cap, read from the terrain round the ring into a 2,048-sample strip. - Not built yet: tacking (a ship on a lane into the wind sails straight into it, sail sheeted in), harbours (a ship in harbour is simply not on the water), ships on polygon seas and lakes, and weather over deserts (Phase 5).
- The weather board is a panel section in every view: upcoming occurrences for the next fortnight, the one in force highlighted, revisions listed under their event with the original struck through. Readers see only what has been published. Writers can schedule an event (published the moment it is added), revise one from now with a reason, delete one, and set the cycle (removed by D-072: the plan is edited in the file, and the board shows only what has been published, to everyone). A selected lake or sea's readout shows its waves now.
19. The scenic tour, as built (D-068, rebuilt by D-085)
D-068 added a tour of nine thirty-second scenes, once the world had particular things in it worth finding. D-085 replaced it, at the author's request, with the world's own flyers: automatic flight, inside and out, a new craft every twelve to eighteen seconds, in an order dealt at random, with the camera pointed at whatever is worth seeing. The caption's border fills in clockwise as a leg runs and closes as it fades to the next (D-094).
- A leg is a craft on a path, and a camera on the craft (
tour/interior-legs.ts,tour/exterior-legs.ts). Twenty-six legs inside and nine outside, each finding its subject in the compiled world — the tallest fall, the highest summit, the biggest ship at sea, a squall Tower gave no warning of, the broadest stand of mesas, the tallest giant, the sun where the clock has it — so they follow the world as it is edited, and a leg whose subject is gone drops out. The craft exists for its leg (D-085); only the ships are the world's own, followed where they are. - The craft: paragliders, gliders, light aircraft and seaplanes, helicopters, airliners,
airships, giant airships near the axis, trains, and outside, spacecraft (
views/craft.ts). A winged craft banks into its turns by the lift the turn needs against the local gravity, so the same turn banks hard near the axis and barely at the floor; airships, ships and trains keep level. - Two rigs. In the craft, looking where the director says, the horizon tilting with the bank when looking ahead; or chasing it, on the far side of the craft from what the director is looking at, so the two share the frame, walked slowly round it over the leg. A chase camera is never under 15 m over the ground.
- The director (
tour/director.ts) scores every point of interest from the craft, twice a second: weight × transmission² × apparent size × how far off the nose, with the transmission integrated through the density profile as the renderer's haze is, and the sightline sampled against the ground for anything a hill could hide. The leg's subject is boosted fourfold. A subject holds the camera at least 4.5 s, bores it after 7, and a turn to the next is eased over 3 s and never swings faster than 0.7 rad/s. It is all worked out when a leg is first played, so a pose is a pure function of time. - Flights hold a steady clearance over the highest ground within 700 m, smoothed, as D-068's did. Aim is taken in the tower's frame, where a straight line is straight: 6 km around, the floor has risen 360 m in the eye's frame.
- Each leg sets the clock and runs it at its own rate — real time, or a labelled time-lapse for the slow giants. The observer's own clock comes back when the tour stops; where they are does not, so stopping leaves them at the view they liked. Outside, the hull turns at the leg's rate whatever the Speed slider says.
- Between legs the view fades to black and waits for the ground — up to four seconds —
so a leg opens on its terrain rather than on the coarse tiles of the moment
(
views/tour-player.ts). - The deal (
tour/plan.ts): every leg once before any twice, no two of a kind of craft in a row, two legs about the same subject kept apart where they can be, and a new deck never opening on the craft the last one closed on. - Controls: Scenic tour in the interior's panel, and in the exterior's, starts and stops it; N skips to the next leg; Esc, a drag, the wheel or a flight key stops it where it is, and so does switching to another view — a tour belongs to its view, and one left running behind another kept its caption on screen under the new one's.
- The screenshot tour photographs the legs, each held at one moment
(
tower.showScene(id, t)inside,towerOutside.showScene(id, t)outside) and planned from a fixed seed, so a leg is the same flight every run. A test checks that every leg the screenshot tool names exists, and flies every leg over the real world, across twenty seeds while it was built: cameras stay in the world, above the ground and out of the beam, move and turn smoothly, keep a chased craft in the frame and bring each leg's subject into the middle of it; outside, nothing touches the hull or the beam's cone, and the grand shot keeps the sun clear of the collector.
20. Phase 5, as built (D-069)
- A desert is a grid and a pile of noise.
terrain/desert.tscompiles what a tile cannot work out alone — the plateau field, the wash network, the pans and their shorelines — on a 250 m grid over the desert (352,000 cells for the Dry Reach, about a second). Dunes and rocky hills are noise, and the tiles make them (field.ts). - The grid machinery is now shared.
terrain/grid.tsholds the grid, the priority flood, the box blur, the neighbourhood walk and bilinear sampling, which the ranges and the deserts both use; the ranges' copies moved there unchanged. - Terraces are cut as the tile samples, not in the compile. The compile stores how far
up the plateau each cell stands, smooth;
terrace()turns that into flat tops and steep risers at whatever detail the tile has. Terraced in the compile, every cliff would be as wide as a grid cell — a 250 m ramp instead of a cliff band. - The plateau threshold is measured, not guessed. The authored coverage is turned into a threshold by taking that quantile of the noise as sampled over this desert, and the same for the dune fields. Thresholds picked against an assumed spread put mesas over everything or nowhere — which is exactly what the first attempt did.
- The washes are routed, not drawn. The same priority flood the ranges erode with runs over the desert's own surface, and where a cell would drain more than 15 km² it is cut by 4 m and more, to tens of metres on the trunks. Two things this taught:
- A flat desert has no drainage at all. The first version fanned out from every edge and gathered nothing, so Tower crowns its deserts: the middle of the Reach stands 150 m above its rim, and everything runs off it.
- Widening a one-cell channel by blurring it averages the channel away. The wash is dilated to its full depth over a couple of cells first, and blurred afterwards for the banks.
- Salt pans are set pieces in the low ground (§4.5 allows exactly this): the lowest cells, spaced apart, each a disc flattened to one level with a crust that frays at the rim, and benches stepped every 20 m around it — the shorelines of a lake that never stood. Both are levels in the world, not in the sand: the shelf under the desert rolls by a hundred metres, and a floor that followed it would not be a floor.
- Tower keeps its rock off its rivers. A mesa field dropped across The Narrows left the whole world's drainage cutting a kilometre-deep slot through it. Rivers now carry a 6.5 km corridor the plateaus keep out of, which is also what a river crossing a mesa field looks like. Beside the lower Long Water the desert's rock still stands over the river, and that section of canyon now runs about 780 m rather than 480.
- Rock is a material, and so is salt. Materials gained a ninth weight,
salt, and bare rock is now claimed by the desert's plateaus, its gravel wash floors, and any steep desert face — sand does not stand at 60°. The ground shader lays strata by elevation, so the bands line up across separate mesas however far apart they stand, which is what makes a field of them read as one eroded formation. - The panel edits all of it (until D-072; now the file does, and the detail card keeps the readout): dune height, spacing, direction and coverage; hills; plateau height, coverage and terraces; washes, pans and shorelines — with a readout of what was actually made ("24% standing rock, 37% dune field, 4 salt pans").
- The tour gained two scenes (§19): past the mesas from the sand, and over a salt pan ringed by its shorelines.
- Cost: the whole world compiles in about 5 s, of which the Dry Reach is about 1 s.
21. Phase 6, as built (D-071)
- A forest is a density field and a placement rule (
terrain/forest.ts). Stems per hectare come from what Tower planted, thinned toward the forest's edge, opened where it left a meadow, and stopped by water, rock, sand and the treeline. The meadow threshold and the width of a meadow's edge are quantiles measured over the forest itself: a band of ±0.02 in noise units left Greatwood as scattered islands, because its noise only varies by about that much in total. - Placement is stratified, as planned. A fixed global grid about one tree apart; each cell hashes its own coordinates for whether it holds a tree, where in the cell, and what it is, accepted in proportion to the density there. So any tile, any view and any visitor works out the same trees with no knowledge of the others, and walking away and back puts every tree back where it was. Tested.
- Four ranges, not the three the plan drew — because at three the wood visibly ended a few hundred metres off, and that edge walked along with you (reported by the author):
- to ~210 m — meshes. A conifer is a tapered trunk and three cones; a broadleaf is a trunk and three lumps; a giant is a column with its crown in the top half. 40–70 triangles each, instanced, 6,000 of them.
- to ~560 m — cards. One quad each, turned to face the eye about the tree's own upright, with the silhouette cut out in the fragment shader: a cone, a round crown or a column. No texture — at that distance a tree is a few pixels and its outline is all of it that survives. About 39,000.
- to 2.8 km — clumps. One card for everything in a 30 m cell, drawn as three crowns over a band: a card per tree would be a quarter of a million at a kilometre and a million at two, and a clump every thirty metres reaches nearly three kilometres for 26,000. At that distance a clump is a thumbnail held at arm's length, which is what a group of crowns looks like from there.
- under all of it, from 620 m out — the canopy skin. Each tile carries a second surface: its own ground vertices, lifted by the canopy's height with a rough top, sinking back onto the ground where the trees themselves take over. It is what closes the wood behind the clumps — a field of cards seen from above is all gaps.
- The skin is opened up from below. Seen from inside the wood it was a black ceiling over everything past the last drawn tree. A canopy is not solid — the gaps between crowns are what let you see any distance through a forest — so from underneath it drops about half its fragments on a pattern fixed to the ground, and what is left is lit as light that has come through the canopy rather than off it.
- Meadows are grass. The forest-floor colour now follows the stems, so a clearing inside a wood is the plain ramp and reads as open grass, which is what makes the meadows visible from the air at all.
- The Tall Wood exists (§4.6): 52 km² of giants, 90–115 m, drawn once from the world's seed — 17 km from the Capital, well inside Greatwood, clear of the water and the rivers — and written into the world file, where the author can move it like anything else. Its crowns are narrow for their height, as a redwood's are: at the planned 0.34 of height the grove closed over into unbroken dark, so a giant carries 0.18 and stands 18 m from its neighbours. Greatwood gives up its ground where a smaller forest stands, so the giants stand instead of the ordinary wood rather than mixed through it.
- Cost: 130–160 fps standing in Greatwood, and a rebuild is about 19 ms for 71,000 trees. Three things bought that: the placement reads the density off the grid the view already samples rather than working the noise out per candidate tree; the clump layer, two kilometres out, is left alone until the eye has moved 170 m; and a box is cut to its circle before the ground is looked at. In flight the near budget drops to a quarter, and is restored in full the moment the eye settles — arriving somewhere used to leave the wood thin until you walked thirty metres.
- The panel sets stems per hectare, the conifer share, the meadows, the treeline and whether a wood is giants, and says what that comes to: "3.1 billion trees over 67,858 km², about 4.7 m apart, canopy 28 m."
- Two more tour scenes: under one of the giants, and low over Greatwood's canopy.
- A settlement clears its ground. The Capital stands at z 270 km, in the middle of Greatwood's belt, and was standing in a wood; a forest now stops at any settlement's footprint and thins over its last fifth. Until phase 7 fills that footprint the Capital is 20 km of grass, which is at least the right kind of nothing.
- Not built: authored stands (a polygon with its own density and species), paths through the woods, and trees on the map. Stands and paths belong with phase 7's router.