ignifx
All guides

Load a heightmap terrain

Terrain

A .terrain.json says how big the ground is, where its heights come from, how it is chunked, and which texture layers blend across it. The Terrain component builds one Babylon Lite mesh per chunk per level of detail, draws them through a single PBR material carrying the terrainSplat surface shader — so shadows, image-based lighting and fog are the engine's — and answers every query a game asks of the ground.

json
// terrain/island.terrain.json{  "format": "ignifx.terrain",  "formatVersion": 1,  "name": "island",  "size": { "width": 512, "depth": 512, "height": 80 }, // metres; height is the full range  "resolution": 513, // samples per side, 2^n + 1  "heightmap": { "source": "island.r16" }, // relative to this document  "chunks": { "size": 64, "lodLevels": 4, "lodDistance": 96, "skirtDepth": 2 },  "layers": [    { "name": "sand", "albedo": "sand.png", "tiling": 10 },    { "name": "grass", "albedo": "grass.png", "normal": "grass_n.png", "tiling": 8 },    { "name": "rock", "albedo": "rock.png", "tiling": 6, "triplanar": true },  ],  "splatRules": [    { "layer": "sand", "height": [0, 6] },    { "layer": "grass", "height": [4, 40] },    { "layer": "rock", "slope": [35, 90] },  ],  "material": { "roughness": 0.9, "metallic": 0 },}

.r16 — little-endian uint16, row-major, resolution × resolution samples, no header — is the canonical heightmap, because it is exact in Node and in the browser alike. An 8-bit source loads with an IGX-1603 warning and terraces visibly: 256 steps over 80 m is a 31 cm stair on every slope. ignifx import heightmap island.png island.r16 converts one.

terrain() declares the materialPlugins rendering feature for you, and a terrain answers heightAt/normalAt/raycast from its height field whether or not a collider exists — so gameplay never has to wait for physics. Omit heightmap and give noise instead for a seeded procedural island; see walk-on-terrain.

typescript
import { Camera, Light, createApp } from "@ignifx/core";import { Terrain, terrain } from "@ignifx/terrain";import type { TerrainAsset } from "@ignifx/terrain";const canvas = document.querySelector("canvas");if (!(canvas instanceof HTMLCanvasElement)) {  throw new Error("ignifx renders into a <canvas> element.");}const app = await createApp({  canvas,  settings: { assets: { root: "assets" }, rendering: { features: { shadows: true } } },  extensions: [terrain()],});// The document loads its heightmap, its layer textures and its generated shader as dependencies.const island = await app.assets.loadAsync<TerrainAsset>("terrain/island.terrain.json");const eye = app.world.createEntity("Main Camera");eye.transform.localPosition.set(0, 70, -160);eye.transform.lookAt({ x: 0, y: 0, z: 0 });eye.addComponent(Camera, { fov: 60, near: 0.5, far: 2000 });const sun = app.world.createEntity("Sun");sun.transform.lookAt({ x: 1, y: -1.4, z: 0.6 });sun.addComponent(Light, { type: "directional", intensity: 3 }).shadows.enabled = true;// The field is centred on the entity: local X and Z span ±size/2, heights run 0…size.height.const ground = app.world.createEntity("Island");const field = ground.addComponent(Terrain, { definition: island, lodBias: 1, receiveShadows: true });await app.start();// Queries are world-space and allocation-free; a query before the document arrives is IGX-1609.app.log.info("height at the origin:", field.heightAt(0, 0), "slope:", field.slopeAt(0, 0));app.log.info("chunks:", field.visibleChunks, "of", field.chunkCount, "draw calls:", field.drawCalls);

Source: examples/recipes/load-a-heightmap-terrain/main.ts