Terrain¶
Rolling hills, mountains and valleys for open levels: a heightmap you
make from a seed or a greyscale picture, then sculpt and paint while the
game runs. It is drawn in chunks that get simpler with distance, and it
collides like any other level geometry, so the player walks on it, balls
roll down it, rays hit it, AI creatures stand on it and nav.build finds
paths over it.
From Lua¶
terrain.create{ size = 240, resolution = 257, height = 22, seed = 3, pos = Vec(0, -8, 0) }
terrain.flatten(Vec(0, 0, 0), 10, 0) -- a level pad at height 0 (for a start, a house)
terrain.raise(Vec(-25, 0, 30), 14, 10) -- a hill 10 m high, 14 m round
terrain.lower(Vec(40, 0, 0), 6, 3) -- a dip
terrain.smooth(Vec(40, 0, 0), 8) -- soften what's there
terrain.paint(Vec(5, 0, 8), 2.5, "dirt") -- a path: layer by name or number (1-4)
local y = terrain.height(10, 20) -- ground height at x 10, z 20
local spot = terrain.ground(Vec(10, 0, 20)) -- the same point, put on the ground
local up = terrain.normal(10, 20) -- which way the ground faces
local steep = terrain.slope(10, 20) -- degrees: 0 flat, 90 a wall
terrain.create{...} takes:
| Field | Default | Does |
|---|---|---|
size |
256 | metres across (it is square) |
resolution |
257 | heights per side; the spacing is size / (resolution - 1) (257 over 256 m = one height per metre). At most 1025 from scripts |
pos |
Vec(0, 0, 0) |
the middle; its y is the lowest the land goes |
height |
20 | metres from the lowest to the highest point |
seed |
1 | the same seed makes the same land on every machine |
feature |
a quarter of the size | metres across the biggest hills |
octaves, roughness |
5, 0.5 | layers of finer detail, and how much each adds (0.3 soft, 0.7 rocky) |
ridged |
false | mountain ridges instead of rolling hills |
image |
none | a greyscale picture next to the scripts or in the game's folder (8- or 16-bit PNG and the rest stb_image reads): black is the lowest point, white height metres up |
flat |
false | no hills at all: sculpt it yourself |
layers |
grass, rock, dirt, snow | { {name = "grass", color = Vec(0.2, 0.32, 0.1)}, ... }, up to four |
rock, rockSlope |
true, 38 | slopes steeper than rockSlope degrees show layer 2 (rock), whatever was painted |
It returns true, or nil and the reason (a picture that isn't there). Calling it again from the same script replaces the terrain.
- One terrain per game. The script that made it changes it; every script
can read it (
height,normal,slope,ground,info). Unloading that script takes it away, as with everything a script makes. - Brushes fall off smoothly to nothing at their radius.
flattenkeeps the inner 60% exactly level and blends the rest. Its height defaults to the point's y. - Sculpting shows at once. The collision follows a quarter of a second after the brush stops (and at least once a second while it goes on), so a script can sculpt every frame.
terrain.layer(layer, {name, color})renames or recolours a layer.terrain.info()givessize,resolution,spacing,corner,centreandowner.terrain.remove()takes it away.
Trees, rocks and bushes¶
terrain.scatter puts copies of a model over the terrain by rule: only
on a layer, only where it isn't too steep, only between two heights.
local pine = models.load("SM_Env_Tree_Pine_01")
local forest, n = terrain.scatter{ model = pine, density = 3, maxSlope = 25, scale = { 0.8, 1.5 },
solid = 0.25, clear = { { pos = Vec(0, 0, 0), radius = 12 } } }
terrain.scatter{ model = models.load("SM_Env_Rock_01"), layer = "rock", density = 6, align = true }
terrain.scatter{ model = models.load("SM_Env_Bush_01"), layer = "dirt", density = 20, scale = 0.5 }
terrain.unscatter(forest) -- all of that group gone again
print(terrain.scattered()) -- how many things in all
| Field | Default | Does |
|---|---|---|
model |
(needed) | a model from models.load |
density |
1 | things per 100 m² where they're allowed (3 = a light forest) |
layer, minWeight |
anywhere, 0.5 | only where that layer is painted at least this much; thinner towards the edge |
maxSlope |
30 | degrees |
minHeight, maxHeight |
any | world heights (trees below the snow line) |
scale |
{0.8, 1.2} |
a random size between the two, or one number |
align |
false | lean with the ground (rocks, grass) instead of standing straight (trees) |
pos, radius |
the whole terrain | only inside this circle |
clear |
none | circles left empty: { {pos = Vec, radius = r}, ... } (a start, a camp, a road) |
solid |
0 | a trunk this many metres thick that blocks the player and bodies (capsule up the model's height) |
seed |
the group's number | the same seed puts everything in the same place on every machine |
max |
5000 | at most this many in the group |
- It returns the group's number and how many it placed. A game has at
most 50,000 scattered things (
ScriptTerrain::maxScattered). Each is an ordinary model instance, drawn instanced and culled outside the view by the model module. - Sculpting moves what stands there with the ground (solid trunks too).
- Points come from a jittered grid (one per cell, anywhere in it), so things never pile up on each other and never line up in rows.
From C++¶
kke::Terrain (kke/Terrain.h) is the heightmap alone: generate,
loadImage, raise, smooth, flatten, paint, heightAt,
normalAt, scatter (the placements by rule), and buildMesh for a
chunk's vertices. kke::TerrainChunks
decides which chunk is drawn at which detail. RigidWorld::Shape::HeightField
collides with exactly the triangles heightAt uses (each cell split on
the diagonal from (x, z) to (x+1, z+1), as Jolt does). kke::ScriptTerrain
puts the three together for Lua, and ScriptModule::terrain() hands it to
a game's C++.
How it is drawn¶
- Chunks of 32 x 32 cells (more on terrains over 513 heights a side, so there are never more than 16 x 16), each its own mesh, drawn only when it's in view (and in the sun's shadow view). Nearby chunks use every height. From 48 cells away they use every 2nd, from 96 every 4th, from 192 every 8th. A curtain (a skirt) hangs from every chunk edge, so neighbours at different detail never show a crack.
- A sculpted chunk is rebuilt at once. Chunks that changed detail as the camera moved are rebuilt nearest first, eight a frame. A new terrain is built whole on its first frame.
- Colour is per height sample: the painted layers blended, rock on steep slopes, and a faint speckle so a field of one colour doesn't look like plastic. Normals always come from the full-detail heights, so far chunks are lit like near ones.
Not yet¶
- Textured layers (grass and rock pictures blended per pixel). The layers are colours today.
- Holes (caves, tunnels), and several terrains side by side for very large worlds (ENG-F64/F78 streaming).
- Saving sculpted terrain with the level, and multiplayer: every machine
makes the same land from the same seed, but sculpting is local. Sculpt
from an
sv_script and every player's copy won't follow yet. - Grass blades drawn by the GPU (thousands per metre, swaying). Scatter places models, which is right for trees, rocks and bushes but not for grass. Scattered things don't sway in the wind yet either.