Scenery composition terrain-first workflow, sampling height, and visual verification
Use this for
Not for
Pairs with: Terrain and heightmaps procedural generation biomes and foliage, Terrain basics ground heightmap and sculpting, Composed props building organic and complex objects from primitives in one prefab, Asset to instances model prefab instance authoring chain, Atmosphere sky lighting fog and time of day, Playtest verification loop proving gameplay executed evidence
Scenery composition terrain-first workflow, sampling, and verification
Why this playbook exists
A run once authored a beautiful meadow, exactly as a landscape artist would, and the delivered screenshot showed floating placeholder cones over muddy ground with invisible foliage, while the model reported the scene "verified" from graph data. Every gap was a composition-and-verification gap, not an authoring-verb gap. This playbook is the craft that closes them: build in the right order, read the ground before you place on it, compose with intent, and LOOK at the result.
terrain-and-heightmaps and terrain-basics teach the op grammar. This playbook is about assembling those ops into a scene that reads.
The terrain-first order (and the BUILD that makes it real)
Author the ground before the things that sit on it, in this order, so each pass operates on the result of the last:
createthe terrain field (width, depth, maxHeight, optional biome + seed).sculptthe macro form: raise hills, lower a valley, then asmoothpass to knock off the terracing so slopes read natural.erosionAFTER sculpting: the current pass is a deterministic smoothing (bothmodevalues run the same thermal-style pass), settling slopes and softening peaks, which is what makes hand-sculpted ground stop looking hand-sculpted. It does NOT carve drainage valleys; carve those withsculptor a riverroadop. Erode the macro form, then paint and scatter onto the eroded result.paintthe material layers: base ground, then a second pass where slope or elevation changes the surface (rock on steep faces, sand at the waterline). Two deliberate passes beat one flat wash; one layer everywhere reads as mud.scatterinstanced foliage over the painted, eroded ground.
Then BUILD ARTIFACTS. The op-log is durable but materializes NOTHING on its own: the deterministic build turns the height edits into heightfield tiles and the scatter into foliage instance buffers, and until it runs the ground does not collide and the foliage does not render. project_apply_terrain_semantic_patch (terrain.patch.apply) now TRIGGERS that build in the SAME call (cost follows change) and receipts it as artifacts built: N tiles, M foliage buffers; terrain_build_artifacts (terrain.artifacts.build) stays as the explicit re-bake if you need to force one. If you author terrain and see zero tiles in the receipt, the surface will render as nothing, so treat that line as your proof the pass landed.
Scatter density is instances per 100 square units, not a fraction
scatter's density is UE/Unity-style: INSTANCES PER 100 SQUARE WORLD-UNITS, max 10000. It is NOT a 0-to-1 coverage fraction. A density of 0.8 means fewer than one blade per 100 sq units and reads as INVISIBLE, which is exactly how a meadow ends up bare. Typical grass is 200-2000; dense ground cover up to ~5000. Use density as a COMPOSITION tool: high density in the meadow foreground, thinning toward the tree line, so the ground has a gradient instead of a uniform carpet. The tool emits a sub-1 density advisory, but author the right number the first time.
Sample the ground before you place a prop on it
The floating-prop failure is placing a tree at a hand-guessed y over sculpted ground: five trees, five different guesses, all wrong, all hovering or sunk. The fix is to READ the surface first.
world_sample_terrain (world.sample.terrain) samples the real surface at up to 64 (x, z) points and returns, for each, the y a prop rests on, the surface normal, the slope in degrees, and covered (false where no terrain reaches that point). It reuses the EXACT height math the runtime draws and clamps to (resolveTerrainGroundHeight + the ctx.terrain.* query host), so there is zero drift between the height you read and the height the ground renders at.
{
"game_id": "<uuid>",
"world_id": "<optional world key or id>",
"points": [
{ "x": -40, "z": 12 },
{ "x": 0, "z": 0 },
{ "x": 55, "z": -30 }
]
}
The workflow: decide WHERE props go (in x/z, from your composition), sample those points in one batched call, then set each prop's TransformComponent.position.y to the returned y (drop the base exactly on the ground, or a hair below to seat it). Read the extras it gives you:
slopegates placement: skip a tree where slope is steep (it would grow out of a cliff); a rock is fine there. Steep-slope gating is composition, not just correctness.normallets you tilt a prop to follow the ground instead of standing plumb on a hillside.covered: falsemeans no terrain there; do not place a grounded prop at that point, or it will float over a hole or past the field edge.
Sample BEFORE placing, always after the terrain build, because you are reading the materialized surface.
Compose with intent, not uniform fill
A scene is a picture. Three moves separate a composed scene from scattered props:
- ONE focal element. A scene needs a subject the eye lands on: a lone old oak on a rise, a ruined arch, a boulder outcrop. Place it deliberately (often off the exact center, near a thirds line), scale it larger than the fill, and let everything else support it. A field of identical trees has no focal point and reads as texture, not place.
- DEPTH LAYERS. Compose foreground, midground, and background. Foreground props frame the view and give scale; the midground carries the focal element; the background (a tree line, distant hills, fog) closes the space. Sample terrain height for each layer's props so all three sit on real ground. Atmosphere (haze/fog thinning distant layers) sells the depth; see atmosphere-sky-and-lighting.
- DENSITY GRADIENTS. Vary density across the scene rather than blanketing it: a dense grass foreground thinning to sparse near a path, a cluster of rocks that scatters out into singles. Gradients read as natural distribution; uniform density reads as a fill tool. Drive this with the
scatterdensityper region and with how tightly you cluster placed props.
For the props themselves: compose multi-part props (a tree, a rock cluster, a bench) from primitives as reusable prefabs (see composed-props-from-primitives), then instance them across the scene with per-instance scale/rotation jitter so copies do not look stamped; for hundreds of one simple mesh use an instanceSet (see asset-to-instances). Reserve generation for the hero focal element.
Verify by LOOKING, not by reading the graph
The trap that produced the floating-cone meadow: the model called scene-graph observers, saw the right entity COUNT and structure, and declared the scene verified. A graph digest is blind to fidelity. It cannot see that a tree is a placeholder cone, that a prop floats a meter off the ground, or that two paint layers read as one muddy smear. Only a rendered frame shows those.
world_observe_entity_view (world.observe.entity_view), THE EYE, is the look tool. For up to 8 entities it resolves each one's world-space bounds from current head and computes the F-key "frame selected" camera (fit the bounding sphere, 3/4 view). Where the posed render lane is wired, the rendered PNG rides back as an IMAGE content part so you actually SEE the framed entity; the receipt also carries each entity's bounds and camera plus a compact scene digest for context.
{ "game_id": "<uuid>", "entity_keys": ["entity.focal_oak", "entity.foreground_rock"], "width": 1024, "height": 768 }
Frame your FOCAL element and a representative prop from each depth layer and look at them: is the tree a real canopy or a cone, does the base meet the ground, do the materials read as distinct layers. Use world_observe_scene (world.observe.scene) for the structural digest (entity counts, bounds), but never let the graph digest stand in for the render check on a scenery brief.
When the render is unavailable, the tool says so, and so must you
world_observe_entity_view is honest by construction: where no posed render lane is wired in the running server, it does NOT fake a frame. renderAvailable is false and renderUnavailable names the exact missing seam (reason, code, missingSeam); each entity's render.status is unavailable. In that state you still get bounds and the computed camera, but you have NOT seen the scene.
Do not report a scene as visually verified off bounds and counts alone. If the render came back unavailable, say in your summary that the composition is structurally in place but was not visually confirmed, and name what a render check would still need to catch (floating props, placeholders, muddy paint). This is the same disclosure norm the placeholder playbooks carry: an unseen scene is an unverified scene, and the honest summary says which it is.
Pitfalls
- Authoring terrain and never seeing a build receipt: the surface will not render or collide. Confirm
artifacts built: N tilesis nonzero. - Placing props before sampling, or sampling before the build: sample the MATERIALIZED surface, then place on the returned
y. - Reading
densityas a 0-1 fraction: sub-1 is invisible. Instances per 100 sq units; grass is hundreds to thousands. - Uniform everything: same density, same prop, same scale, no focal point. That is fill, not composition.
- Declaring the scene verified from
world_observe_scenegraph data alone. Look with THE EYE, and if the render is unavailable, disclose that it was not seen. - Scattering a tree or flower foliageKey and expecting a canopy: only
grass/tall_grassandrock/boulderhave real scatter geometry; other keys render as placeholder cones. Place trees as model entities and reserve scatter for ground cover.