Terrain
Jackdaw’s terrain is a heightmap-backed mesh, chunked for
streaming and edited with brush-style sculpt tools. The crate
that does the work is jackdaw_terrain; if you want the
actual data structures, the entry points are
Heightmap, apply_brush, and build_chunk_mesh_data.
Add a terrain
Add > Terrain in the hierarchy. You get a flat heightmap
component on a new entity, with a chunked mesh underneath.
Resolution and physical size are properties on the
Terrain component, editable in the inspector.
Sculpt
Select the terrain, then pick a sculpt tool from the toolbar or the terrain panel. Available tools:
- Raise / lower. Add or subtract height under the cursor.
- Flatten. Drag heights toward the height under the click point.
- Smooth. Average heights inside the brush radius.
- Noise. Add procedural noise inside the brush radius; good for breaking up flat areas without sculpting by hand.
Brush radius and strength sit in the toolbar. The brush preview ring tracks the cursor so you can aim before committing.
Ctrl+Z undoes the last stroke. Each contiguous drag is one
undo entry, not one entry per heightmap sample.
Erosion
The erosion pass simulates hydraulic erosion across the whole
heightmap. Adjust iteration count, evaporation rate, and
sediment capacity in the panel; click Run. It is a
one-shot operation, not a real-time tool.
This is the slowest thing in the terrain workflow, since it runs on the CPU and rebuilds every chunk mesh when it finishes. Save before you click. We do not have a cancel button yet.
Paint channels
Choose the paintbrush in the terrain toolbar to edit integer
channels such as biome, ground type, or buildability. Add a
channel and one or more palette values in the Paint
Channels section, select the value to write, and drag over
the terrain. Hold Ctrl while painting to restore value 0.
Show Painted Values tints the terrain with the active palette so the stored data is visible even before a game material consumes it. New channel and palette entries receive generated names and colours; project-specific descriptor names, integer widths, labels, values, and colours can also be authored directly in the scene document.
Quantization
Enable quantization when the game needs a fixed metric grid or terraced elevations. Cell Size controls the world-space distance between samples and Height Step controls the elevation interval. Sculpt, generation, and erosion snap new changes while quantization is enabled. Click Apply once to snap heights that existed before it was enabled.
Turning quantization off stops future snapping but does not alter heights that are already stored.
Scatter
The Scatter section places model instances across the selected terrain. Add one or more model assets, then configure density, spacing, scale, yaw, normal alignment, and an optional paint-channel mask. A seed produces the same placement for the same terrain and channel data.
Re-running the same scatter group replaces untouched generated instances. Instances moved, rotated, or scaled by hand are preserved. The whole run is a single undoable edit.
Sidecars and export
Scene files keep the small terrain descriptor, while heights
and per-cell channel values are stored beside the scene in
versioned .jdterrain files. Save and move those sidecars with
the .bsn scene that references them.
For a headless runtime or another engine, export the authored terrain with:
jd export-terrain path/to/scene.bsn --out path/to/export
The export contains height and channel images, a manifest, and
placed-scene data. Quantized projects normally keep cell size
and elevation step on the terrain; unquantized scenes can pass
--cell-size and --elevation-step together for an export-only
grid. Add --raw-heights when the consumer also needs the raw
height buffer.
Export format contract
This is a cross-repo contract: an importer in another repo is built against it, so treat the shapes below as stable.
manifest.json,format_version: 2. Bumped whenever a field is added, removed, or reinterpreted; an importer should check it and refuse (or degrade explicitly) on a version it does not understand, rather than assume the shape it expects.heightmap.pngis a 16-bit grayscale PNG. Every pixel decodes to a world-space height viaheight = manifest.heightmap.base_m + pixel * manifest.heightmap.step_m(encoding: "unsigned-steps-from-base"). Quantized exports setstep_mto the terrain’s elevation step; unquantized exports derivestep_mfrom the actual authored height span (not frommax_height_m, which is a configured ceiling and can differ from the real data range).- Each paint channel is its own PNG (
channels/<name>.png, 8- or 16-bit depending on the channel’s element width) plus a manifest entry:name,file,bit_depth,element("u8"/"u16"), andpalette– a list of{ value, label, color }entries,coloras#rrggbb. Channel names are guaranteed unique in one export: the writer refuses the whole export if the scene’s channel names collide, either exactly or after filename sanitization. placements.json, its ownformat_version: 1, lists every scattered / placed instance:name,asset(nullable),translation_m/rotation_quat/scale, andcomponents(a free-form JSON map of any extra authored component data on that instance).heights.f32, present only with--raw-heights: the raw height buffer as little-endianf32, row-major, unquantized and unscaled – for a consumer that wants the source values rather than the quantized PNG encoding.
Chunking
Chunks are 32 cells per edge (src/terrain/mod.rs::CHUNK_SIZE).
Edits only rebuild the chunks that overlap the brush,
which is what keeps sculpting fast on large heightmaps.
There is no LOD or frustum streaming yet; every chunk
renders at full resolution.
Common gotchas
- Mesh shows seams between chunks. Normals are computed per chunk. The boundary samples should match across chunks; if they don’t, an edit straddled the boundary and one side never rebuilt. Touch both sides with the smooth tool to force the rebuild.
- Erosion result looks wrong. Iteration count is the knob to tune first. Defaults aim for a generic mountain; rolling hills want fewer iterations and a higher evaporation rate.
- Standalone game shows no terrain. Two separate causes
land on the same symptom. First,
jackdaw_runtimedoesn’t pull injackdaw_terrain: if your game needs terrain at runtime, addjackdaw_terrainto your standaloneCargo.tomland bring whatever plugin / systems you want into your game’s plugin alongsideJackdawPlugin. Second, even with the crate present, a terrain’s heights and paint channels live in a.jdterrainsidecar next to the.bsnscene, not in the scene file itself (see “Sidecars and export” above) – if you load a.bsnscene directly at runtime rather than through thejd export-terrainpipeline, every referenced.jdterrainsidecar has to ship and load alongside it, or the terrain reads as flat.