Optional terrain authoring
For authored terrain, install @threenative/terrain as a development dependency:
On this page
npm install --save-dev @threenative/terrainSearch engine_search_capabilities for terrain authoring and inspect the returned
engine_capability_detail before creating a terrain generator. Use the installed
terrain operations rather than writing noise, erosion, masks, roads or rivers again.
Asked for a ready world? Start from a starter kit
A kit is a whole playable world as game source: recipe, bake script, CC0 art, terrain surface,
sky, props and colliders. @threenative/terrain ships them under starter/<world>/ (today:
forest). Copy one into the game, bake, add it to a scene:
cp -r node_modules/@threenative/terrain/starter/forest src/terrain/forest
node src/terrain/forest/bake.mjs --out assets/terrain/forestThen await addForest(ctx, player) in Scene.load and ctx.add(forestDaylight(ctx.camera)) in
Scene.enter; the kit's README.md says what each file decides. Edit recipe.json for layout,
COLLIDERS in world.ts for what stops the player, surface.json and sky.ts for the look, and
re-run the bake. The bake writes the engine's WorldCells package (bakeWorldPackage).
Generate once, play the bake
Run this in an authoring/build script, outside the game loop:
import { Terrain, Mask, bakeMesh, bakeTerrain } from "@threenative/terrain";
const terrain = new Terrain({ size: 512, resolution: 257, seed: 42 })
.noise({ id: "hills", amplitude: 30, scale: 180 })
.flatten({ id: "spawn", height: 8, mask: Mask.circle([0, 0], 12) });
const state = terrain.evaluate();
const mesh = bakeMesh(state);
const collision = bakeTerrain(state).collision;Persist the recipe with terrain.toJSON() for later editing, and save the baked
arrays for the game. Stable layer IDs let an agent replace an operation instead of
appending duplicate layers. Terrain.fromJSON(recipe) restores the authored stack;
terrain.applyPatch(commands) applies a validated edit atomically.
Keep the game's appearance editable
toGeometry(mesh) from @threenative/terrain/three creates an ordinary caller-owned
Three.js BufferGeometry. Choose its material, lighting, water and vegetation in
this game's src/render/. Colours are only baked when you provide an explicit
palette; the package does not choose a material or a renderer. Dispose the geometry
and material when the scene releases them.
For collision, reuse Heightfield from @threenative/core/world and
CollisionShape3D.heightfield from @threenative/physics. Supply the same baked
height samples and extent to rendering and collision. Heightfield dimensions count
vertices; toColliderHeights() converts the canonical row-major samples once for
Rapier. One unit is one metre. Bilinear heightAt() is an interpolation aid; compare
actual triangles and physics contacts when verifying ground alignment.
Keep terrain evaluation, erosion and editor tooling out of the shipped game graph.
The runtime consumes baked arrays and assets. The legacy encodeGLB/makeExport
terrain export contains the terrain mesh only; it is not a full-world export of
vegetation, rocks, water and imported assets.
Optional live terrain editor
Install the authoring dependency above; Vite remains the project's development
server. Save terrain/world.json as { "version": 1, "recipe": terrain.toJSON() }.
Add the optional plugin to this project's Vite configuration:
import { resolve } from "node:path";
import { terrainEditor } from "@threenative/terrain/editor/server";
const editor = terrainEditor({ documentPath: resolve("terrain/world.json") });
// Add editor to the existing Vite plugins. Bind the server to 127.0.0.1/localhost.
// Add "@threenative/terrain/editor" to optimizeDeps.exclude so its adjacent
// evaluator worker remains a separate module.The project supplies /terrain-editor/index.html. Its browser entry calls
mountTerrainEditor from @threenative/terrain/editor with createView(host, controller) and
an explicit eight-channel material colour palette. Implement the exported
IEditorView around this game's ThreeNative scene and src/render/ helpers; keep
that authoring entry separate from src/game.ts. The addon reuses the recovered
brush, layer, recipe, cancellation and data-export controls around that view.
It does not create a second renderer or silently choose the game's appearance.
The view's setDocument(document, revision) applies metadata changes separately
from its terrain update(state) path. Keep camera and placement edits out of the
evaluator worker.
Track the accepted document and the successfully rendered recipe separately. While
a rebuild is pending or has failed, metadata must retain the last valid scene and
rendered revision; apply deferred transforms when that recipe renders successfully.
After the server listens and the route is ready, await editor.activate() returns
editorUrl, projectId, sessionId and the current content-hashed revision.
The development server also prints the bound editor URL. Present that returned
link; do not invent a port or give a user 0.0.0.0. On a remote machine, configure
viewerUrl with the existing port-forwarded editor URL; without forwarding the
loopback link is local to that machine. Activation probes the advertised route;
an unreachable or failed private forward returns an error. Only the explicitly
configured viewer host/origin is additionally trusted; the server still binds to
loopback.
An agent can edit the same document without clicking the GUI:
import { TerrainEditorController } from "@threenative/terrain/editor";
const controller = new TerrainEditorController(activation.editorUrl);
const current = await controller.snapshot();
await controller.commit({
baseRevision: current.revision,
commands: [{ op: "update", id: "hills", patch: { params: { amplitude: 35 } } }],
});Stale bases conflict. Read the latest snapshot and merge the intended semantic
edit; do not overwrite another agent/human's work with an old document. Complete
atomic JSON saves also update the shared view. Invalid disk saves retain the last
valid preview and show a diagnostic; restore a valid save before submitting more
patches. Editor imports, workers, watchers and the document stay out of the game
runtime. Bake committed data before handing it to the game. The editor's GLB action and
exportWorldGLB from @threenative/terrain/export write the whole resolved world (terrain,
props, final transforms, embedded PBR maps); the root encodeGLB stays terrain-only. The full
operation list, units and patch rules ship in node_modules/@threenative/terrain/AGENT_GUIDE.md.
For individual props, save placementOverrides in the authoring document, keyed
by evaluated placement id. Each entry contains position: [x,y,z], a unit
quaternion: [x,y,z,w], positive scale: [sx,sy,sz] and grounding: true|false.
Use validatePlacementOverrides and applyPlacementOverrides from the headless
addon. Grounding defaults on; the game uses its actual model bounds and terrain
triangles. A free Y lift records grounding: false and still measures clearance.
The resolved state and bakeTerrain retain transforms; the game must consume
them. Never use an instance-buffer index as a saved key. Keep unmatched overrides
visible until the author explicitly removes or reassigns them.