Guide14 min read8 sections

Editable heightfield terrain

docs/TERRAIN.mdSource updated 2026-10-27
Markdown
On this page 8 sections

packages/terrain owns a versioned, editable heightfield source and bounded chunk compiler. The generated chunks use the existing model, content streaming, renderer and static triangle collision paths. They can be exported as an ordinary world and activated or published through the existing authoring API.

The atelier now retains terrain sources in the editable document and offers raise/lower and material brushes, local brush undo, preview, saved undo/redo, shared Apply and a meadow starting template. Published runtime worlds contain the generated meshes; archived editable documents retain the source. Sample-level collaborative brush saves are connected below. Terrain streaming covers per-traveler chunk residency; broader scale, terrain LOD transitions and final qualification remain open. Voxel ground provides volumetric Dig/Fill authoring. Trees and shrubs, seeded rivers, water and swimming have separate authoring and runtime contracts. Final environment art remains open.

#Collaborative brush saves — October 27

Two builders can sculpt or paint different parts of the same saved patch from the same revision, then Preview ground → Save changes in either order. The later save retains both changes. Sculpting and painting the same area use independent height/surface channels. If both change the same sample in the same channel, the stale save conflicts and preserves the current landscape and the unsaved draft. Reload before combining that overlapping work.

Run npm run dev:editor, open its private link in two browser profiles, and save an Open meadow. Load it in both editors. Apply a small brush near Across/Along 6/6 in one, and 24/24 in the other, preview both, then save both. The second editor reports that independent changes were merged. Saved Undo/Redo reverses only that builder's accepted sample changes, retaining the other hill, including after server restart. Use Apply saved revision to share the saved landscape with travelers, or publish an immutable playtest.

The public edit-terrain command contains the patch ID, a layout checksum and up to 512 compare-and-set sample changes. terrainBrushCommands prepares it from the saved document and edited patch. Each sample names its lattice key, expected value, replacement value and optional surface channel. The existing authorized world transaction, retained revision lineage, per-actor history and durable retry receipts own admission, conflict detection and publication. Overlapping ABA edits remain conflicts even if a newer edit restores the old value. Expired world merge history refuses a stale save rather than guessing.

The layout checksum fences the patch's identity, name, location, spacing, chunk selection, recipe and environmental/streaming settings. Moving, regenerating, removing or changing settings still uses set-terrain and invalidates stale brushes. Saves with more than 512 changed samples also use whole-patch replacement. Brushes and structural commands cannot be mixed in one transaction. Incoming sample records are bounded, deduplicated and checked against current source data; malformed coordinates, impossible material weights and out-of-patch samples reject atomically. Clients cannot publish their own generated meshes.

Each accepted brush batch advances source revision and retains a bounded source history entry. Undo restores sample values and the generated landscape while advancing history; it does not rewind the source revision/checksum. Original whole-patch saved history retains its exact-source inverse behavior. Source, models, foliage and distant overview are still derived and validated together; shared Apply retains the existing collision preparation barrier.

#Try the editor

Run npm run dev:editor and open the printed private link. Choose New world → Open meadow, name it, preview it, then Save changes. The meadow starts with four contiguous chunks and two gentle hills.

Open Terrain. Click the height map, or move the brush with the arrow keys and press Enter. Raise/lower, radius and strength control each stamp; Across/Along position the brush precisely. Undo brush reverses unsaved strokes. Preview ground shows the candidate in the atelier; Save changes retains its source and meshes together. Cancel leaves the saved draft unchanged; Discard removes an atelier preview. Saved Undo/Redo survives editor and server reloads.

Choose Paint to blend grass, earth, stone or sand. Paint strength controls how much of the selected layer each stamp adds. The map previews both color and height; painting does not change collision. Grass blends back toward the original ground.

Expand Generate a landscape for Rolling hills, Weathered ridges or Dry basin. Choose a whole-number seed and relief from 0.25 to 8 metres, then Generate. This replaces the patch’s heights and paint while keeping its identity, position and chunk layout. A flat clearing surrounds the arrival point. Undo brush restores the previous surface; Cancel leaves the saved document unchanged. Preview and Save use the same workflow as brushes. Reopening a saved recipe restores its controls. Generating it again removes later brushes and reproduces the baseline.

Expand Meadow grass and enable Grow meadow grass. Set a seed, coverage, height and view distance, then Preview ground and Save changes. Grass follows the ground and its grass material weight; steeper slopes and arrival areas stay clear. Reopen Terrain to change or disable it. Cancel and Discard leave the saved draft unchanged; saved Undo/Redo also restores the grass recipe.

Start the shared room, invite another traveler, then use Apply saved revision after sculpting. The existing collision preflight can reject changes that would overlap a traveler. Move clear of the edited area and retry. Playtest publishes an immutable copy.

Existing places can add a 16×16-cell patch. Use Patch position to place it in open space; it does not automatically replace an existing floor. A place has one editable terrain surface, with at most four patches and sixteen total chunks per document. Generated ground objects have their ordinary transform/material controls disabled; use Terrain to move or reshape the patch.

The editable WorldDocument.terrain field owns each patch's ID, name, place, origin and heightfield source. set-terrain is a standalone authoring transaction; the server derives and validates all generated meshes and objects from that source. Exact saved inverses preserve object order, identities, models and prior world format. Generic edits cannot detach a generated object or reuse its model. Concurrent edits to the same patch conflict; unrelated decoration changes can merge through the existing authoring journal. Structural changes retain this whole-patch contract. Ordinary source-only brushes use the sample-level journal described below.

The CLI can also create a source-retaining document directly:

Terminal
npm run author -- template meadow "Our meadow" .data/meadow.editable.json

#Try the file workflow

Run from the checkout with Node 22.12 or newer. These commands create new files and refuse to overwrite an existing path:

Terminal
npm run terrain -- init .data/meadow.base.json
npm run terrain -- sculpt .data/meadow.base.json 16 16 8 1800 .data/meadow.raised.json
npm run terrain -- paint .data/meadow.raised.json 16 16 6 earth 220 .data/meadow.painted.json
npm run terrain -- build .data/meadow.painted.json .data/meadow.world.json
npm run author -- import .data/meadow.world.json .data/meadow.document.json
npm run author -- check .data/meadow.document.json

init creates four flat chunks spanning 32 by 32 metres. Sculpt arguments are lattice X, lattice Z, brush radius in samples, and signed height change in millimetres. This example raises a smooth hill by 1.8 metres at the four-chunk junction. A negative change lowers the ground. Paint arguments are lattice X/Z, radius, material name, and integer strength from 1 to 255. Each operation records a unique ID in the resulting source history. npm run build:author also emits author-dist/terrain.js, with the model compiler and dependency notices, for use outside this checkout.

For exact replay and undo, npm run terrain -- apply <source.json> <request.json> <new-source.json> accepts the public request. Fill baseRevision and baseHash from the source; operation names a retained history entry:

json
{
  "kind": "undo",
  "id": "undo-meadow-hill",
  "baseRevision": "1",
  "baseHash": "<source sha256>",
  "operation": "<sculpt operation id>"
}

Undoing that undo is a redo, provided no newer edit changed its samples. In-memory callers use createHeightfield, readHeightfield, editHeightfield, HeightfieldCompiler and heightfieldWorld. Reuse a compiler instance across revisions to retain unchanged chunk artifacts. Each independent CLI build starts a fresh compiler.

#Repeatable landscape recipes

Terminal
npm run terrain -- generate ridge 104729 .data/ridge.source.json
npm run terrain -- sculpt .data/ridge.source.json 20 16 4 500 .data/ridge.edited.json
npm run terrain -- regenerate .data/ridge.edited.json .data/ridge.baseline.json
npm run terrain -- build .data/ridge.edited.json .data/ridge.world.json

generate accepts hills, ridge or basin, a decimal integer seed from 0 through 4294967295, and a new output path. Defaults are four contiguous chunks, one-metre spacing, 3.5-metre relief and a flat arrival clearing at lattice (8,8). regenerate reproduces the saved recipe baseline in a new file, removing subsequent brushes. Both commands are included in the portable terrain.js bundle.

generateHeightfield accepts a complete heightfield-landscape.1 recipe and a chunk selection. The recipe includes style, seed, amplitude in millimetres, feature size in lattice cells, a center and a clearing position/height. The generator uses fixed integer hashing and quantized value noise. Generated grass, earth, stone and sand weights depend on slope, height and style. Global sampling gives neighboring regions the same shared samples. A rounded clearing with a smooth transition retains safe arrival footing; a moved patch recalculates that clearing when generated in the editor. Existing collision preflight still protects travelers elsewhere.

Format 3 stores the complete recipe, a digest of its generated baseline, baked samples, independent sample versions and ordinary brush history. Admission verifies both the complete source checksum and the recipe receipt. Sculpt/paint retains the receipt; it names the baseline, not the subsequently edited sample data. Generation creates a new baseline at revision 1 with no brush entries. Local whole-source undo and saved patch history retain the replaced baseline. Formats 1 and 2 remain supported.

The recipe compiler retains at most four immutable outputs keyed by the complete recipe, chunk selection and spacing. It owns inputs before asynchronous work; clearing it cannot be undone by an older pending request. Fixed golden hashes and browser/Worker comparisons pin this generator version. Algorithm changes that alter output require a new generator profile and a compatible reader; a seed alone is insufficient.

Authoring also retains two completed terrain compiler states, at most 32 chunks and two bounded atlases. This avoids rebuilding the same before/after sources during saved inverse validation. Every lookup first admits the complete source and verifies its checksum. Patch names, positions and object projections are rebuilt independently; no cached output authorizes a different source.

These are bounded landscape starting recipes. Seeded river recipes extend these landscapes with a carved channel, water and swimming. Trees and shrubs add seeded botanical models and trunk collision. Voxel editing provides volumetric ground. Streaming terrain, terrain LOD transitions and a finished environment art treatment remain open.

#Meadow grass

A terrain patch may retain a heightfield-grass.1 recipe: unsigned 32-bit seed, integer coverage from 1–100%, nominal height from 0.15–1.4 metres and view distance from 8–120 metres. Changing the terrain source, paint, position or grass recipe regenerates its cosmetic projection through the same set-terrain transaction. Generated foliage is protected from independent edits while the source is retained. Saved undo restores the source, baked output and previous world format together; disjoint atmosphere edits survive.

Each cell offers two stable seeded candidates. Coverage is multiplied by the average grass material weight at the four corners; candidates on slopes above 0.85 are rejected. Roots follow the mesh's exact triangle diagonal. Heights, orientation and color/wind variation are seeded, and baked coordinates are rounded to four decimals. A rounded rotation is clamped below one full turn so valid seeds cannot produce inadmissible angles. Placement is independent of chunk ordering and shared regions reproduce identical samples. Up to four immutable outputs are cached with owned inputs, source validation and a clear-generation fence.

World format 19 stores the meadow-grass.1 baked instances; presentation format 12 carries them to main-thread or Worker graphics. Older formats remain supported. The runtime also clears a 3.25-metre radius around the current spawn and every authored arrival. Recipe clearings use the same radius. Grass is cosmetic: it creates no gameplay entities or collision bodies, changes no traveler footing, and does not consume ordinary object/player instance slots.

A patch has at most 8,192 baked tufts. The renderer selects nearby visible tufts within fixed budgets: 4,096 on High, 2,048 on Balanced and 1,024 on Performance. Balanced uses 80% of the authored view distance and Performance 60%. Tufts shrink at the distance/capacity boundary. Each tuft has three bent blades, nine triangles in total, with anchored roots and shared-time wind. The pass uses one optional 256 KiB instance buffer, one draw and no textures; it adds no per-frame GPU allocation. Base/tip colors, ambient/sun lighting, fog and portal clipping are supported. Grass currently does not cast or receive scene shadows, and does not use custom vegetation models.

Grass acceptance covers deterministic source admission, quality/distance budgets, streamed world content, unchanged physics/checkpoints, durable undo, editor controls, two-traveler Apply/publication and main/Worker rendering with exact undo/device recovery. This is the first vegetation layer. Trees and shrubs provide the next layer. Expanded species libraries, independent vegetation masks, broader physical-device/long-session qualification and the final environment art treatment remain open.

#Source and edit contract

Format 1 stores canonical integer millimetre heights on a global lattice. A chunk has 16×16 cells and borrows its 17×17 corner samples. Shared edges and four-way corners have one source value. Missing samples are zero. Spacing is one of 0.25, 0.5, 1, 2 or 4 metres; chunk coordinates range from −64 to 64. A source contains at most 16 chunks and 4 MiB of encoded data. Height is bounded to ±512 metres.

The circular brush has a smooth cubic falloff, an integer radius of 1–8 samples and a nonzero signed strength of at most 2,000 mm. A change outside the source or beyond a height budget rejects atomically. Edits copy their request before asynchronous validation and require the current revision and checksum. Exact duplicate operation IDs replay without applying twice; reusing an ID with different input rejects.

At most 32 edit records are retained. Every changed sample records its last revision, including changes back to zero. Undo checks both the expected value and that revision. An unrelated edit survives; an intervening edit to the same sample conflicts even if it later restores the old height. Expired undo targets reject. Replaying an evicted old operation against its original base also rejects. These are owned source transactions. Editor saves persist the complete source through the existing authorized document/history service; this package itself does not provide network authorization.

#Material layers

The first paint operation upgrades the owned source to format 2. Legacy format-1 bytes, sculpt arithmetic and unpainted compiled artifacts remain unchanged. Three integer weights (earth, stone, sand) are packed into a 24-bit sample; grass is the remainder to 255. Zero entries are omitted. Smooth brush falloff blends toward the selected layer, with deterministic largest-remainder rounding. Height and surface channels retain independent sample revisions, so undoing a paint operation preserves later sculpting, while a newer overlapping paint edit conflicts. Editor saved history still operates on the whole patch.

heightfield-painted.1 bakes the shared linear-light palette into one RGBA texture covering the patch bounds. All chunks reference identical texture bytes, mip chains and global UV coordinates at their borders. Resolution is eight texels per lattice cell, capped at 512 pixels on either axis and reduced further when the worst-case repeated mip payload would exceed 12 MiB in an embedded document; widely separated chunks share that same bounded atlas and therefore lose effective paint resolution. Color layers currently share the terrain's roughness; per-layer normal/roughness maps and painted vegetation masks remain future work.

Paint changes rebuild each chunk's model artifact because they reference one shared map. Geometry and collision stay identical, and the renderer deduplicates the shared map and its material fallback. Sculpt-only changes retain the atlas and the existing geometry/normal-halo invalidation. Returning every surface sample to grass emits the exact original untextured artifacts. This is bounded material painting, not terrain LOD or chunk streaming.

The compiler retains one private atlas across independent forks. Catalog validation reuses only successfully admitted textures keyed by their complete canonical data, with at most eight entries and an 8 MiB retained-string budget. A matching claimed hash alone cannot bypass validation. Streamed runtime admission retains its cancellable work path. The editor passes validated model identities from scene preparation to GPU installation and reuses the already-prepared structural preview; a matching save retains the exact installed preview, while changes to content, place, behavior preview, scene identity or renderer identity invalidate that reuse. The server still validates saves independently.

#Geometry and safety

heightfield-mesh.1 emits upward-wound triangles with a fixed cell diagonal. Adjacent chunks share exact positions, world-aligned UVs and central-difference normals. A one-sample halo participates in the compilation fingerprint, so editing just inside a boundary also refreshes its neighbor's normals. Unaffected chunks retain their immutable model artifacts. A successful complete compile replaces the bounded cache; cancellation, supersession and failures cannot publish partial candidates.

The compiler generates a portable GLB and admits it through the normal model compiler. Terrain collision derives from the same base triangles as the visible surface. The existing simulation revision barrier prepares replacement collision independently and rejects a surface that overlaps a player. The old live scene remains available on rejection. Shared activation still uses the existing coordinated room revision protocol; the editable terrain source travels with the saved authoring document, while the runtime consumes only its generated projection.

The native study renders a four-chunk meadow, sculpts it, restores exact pixels through undo, repeats model replacements and reconstructs it after device loss. Headless physics walks across a raised seam, verifies rejected overlapping ground leaves the original checkpoint intact, and restores accepted terrain collision from a native checkpoint. The shared browser journey applies and undoes generated terrain worlds through the authoring API and enters an immutable publication.

Acceptance evidence records the checks, source hashes and rendered fixture. These are bounded software-WebGPU and local physics observations. Large-world streaming, terrain-specific LOD seam qualification, long-session/device measurements and the final environment art pass remain incomplete. Milestone 15 remains open.

Editor acceptance and retained compatibility failures covers source-retaining creation, save/reload/undo, shared Apply, publication and responsive controls. The existing published-world restoration reload readiness check also fails on the preceding commit and remains open.

Painting acceptance includes source/texture integrity, sixteen-chunk document budgets, detached CLI tools, native rendering, durable undo and shared application. It retains the later overall shared-journey timeout after successful gameplay assertions; sustained timing acceptance remains open.

Landscape recipe acceptance records reproducible generation, durable source history, standalone tools, main/Worker rendering and shared publication. The initial save and shared-journey timing failures remain recorded alongside the passing final campaign.

End of Terrain