Onceworlds
Engine · Terrain

Terrain

Ground from presets or heights, solid with physics, sculpted in Build or in code.

Don't build ground out of boxes or a giant plane. The API reference ("Terrain3D") has every field and function.

The default path: one Terrain3D

import { Transform } from '@onceworlds/engine';
import { Terrain3D } from '@onceworlds/engine/modules';

world.spawn([Transform(), Terrain3D.preset('hills', { seed: 7 })]);

It is centred on its entity, coloured by height and slope (grass, rock on steep slopes, sand low down, snow up high) and, with Physics3D, solid: characters walk on it, cars drive on it, balls roll down it. Nothing else to add. In a scene it is a node of kind Terrain3D, and there its preset works by name (it is the landform).

Preset For
hills Rolling green land: open worlds, farms, golf, adventure. The default.
mountains Ridges and peaks with snow (192 m, 48 m tall): climbing, skiing, a backdrop.
island Land in the middle that falls below 0 at its edges: add Water3D.preset('ocean') at 0.
canyon A winding gorge between stepped mesas: racing, westerns.
dunes Sand ridges: deserts.
flat Ground to sculpt in Build, or for a level of your own on top.

Another seed is another landscape of the same kind. size is metres along x and z, height how tall the landforms are, resolution the cells a side (128 by default: 1 m cells on the default 128 m). Keep cells about a metre and resolution at 256 or less unless the game needs more: a 512 terrain takes a few hundred milliseconds to make and four times the memory of a 256.

Putting things on the ground

import { terrainHeightAt } from '@onceworlds/engine/modules';

for (const [x, z] of treeSpots) {
  const ground = terrainHeightAt(world, x, z);           // { height, entity } or null off the terrain
  if (ground) world.spawn([Transform({ position: [x, ground.height, z] }), ...tree()]);
}

It is the same surface physics uses, to the centimetre, and cheap enough to ask every frame (a bot's footing, a shadow blob).

Its look

The four layers have colours (grass, rock, sand, snow) and may have pictures (grassMap... snowMap: image assets, or the library's Library.texture('grass'), 'rock', 'sand', 'snow', repeating every tile metres). sandLevel (metres), snowLine (a share of height; 1 or more: no snow) and rockSlope (degrees) say where they go. toon: true for a cartoon look. Painting in Build lays a layer wherever you want it.

Sculpting in Build

Select the Terrain3D node: the Terrain panel opens and the 3D viewport's toolbar gets Raise, Lower, Smooth, Flatten, Paint and Erase Paint (keys 1 to 6). Drag to sculpt: Shift does the opposite (raise lowers, paint erases), [ and ] resize the brush, Esc takes the stroke back, and each stroke is one undo step. The result goes in the scene as two compact codes, heights and paint: leave them to Build (a few strokes are a few hundred characters).

Changing it while the game runs

import { sculptTerrain, terrainOf } from '@onceworlds/engine/modules';

function crater(world, terrain, x, z) {
  const data = terrainOf(world, terrain.id);
  const at = terrain.get(Transform).position;
  for (let i = 0; i < 5; i++) sculptTerrain(data, { tool: 'lower', x: x - at.x, z: z - at.z, radius: 5, strength: 1 }, 0.1);
}

Brushes work in the terrain's own space (metres from its centre). Drawing and the collider follow at once. In a multiplayer game send the brush to every page (an event or rpc) and apply it there; don't replicate heights. resetTerrain(world, entity) undoes live changes.

Heights of your own (a heightmap you generated): Terrain3D.heightmap(heights, { size: [200, 200] }) with (n + 1) × (n + 1) numbers, row after row along +z.

Limits