ignifx
All guides

Scatter foliage on terrain

Terrain

TerrainScatter places instances on the terrain it shares an entity with — or on any ancestor's — by seeded rules: a density in instances per square metre, the splat layers a candidate may stand on, and the slope and height bands it must fall in. Placement is a pure function of the seed, the height field and the rules, so a headless test asserts the same grass a device draws.

It places once, into the InstancedMeshRenderer it requires, and maxInstances sizes that renderer's GPU buffer before the scene is registered — so set it before app.start() and call regenerate() after changing a rule. A sculpt regenerates by itself.

createFoliageMaterial builds a shader material with a vertex-stage wind, because a surface shader's displace hook cannot read the clock: Babylon Lite's plugin uniforms are fragment-stage only. That is the trade — wind, alpha cutout and cheap Lambert lighting, but no image-based lighting, no fog, and shadows cast (as solid cards) rather than received.

typescript
import { Camera, Light, MeshAsset, createApp } from "@ignifx/core";import { Terrain, TerrainScatter, createFoliageMaterial, terrain } from "@ignifx/terrain";import type { TextureAsset } from "@ignifx/core";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()],});const island = await app.assets.loadAsync<TerrainAsset>("terrain/island.terrain.json");const card = await app.assets.loadAsync<TextureAsset>("terrain/grass-card.png");const eye = app.world.createEntity("Main Camera");eye.transform.localPosition.set(0, 12, -30);eye.transform.lookAt({ x: 0, y: 4, z: 0 });eye.addComponent(Camera, { fov: 60, far: 1500 });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;const ground = app.world.createEntity("Island");ground.addComponent(Terrain, { definition: island });// Alpha-cutout cards that sway; `wind: null` is a still, cheaper material.const grassMaterial = await createFoliageMaterial(app, {  albedo: card,  alphaCutoff: 0.4,  doubleSided: true,  wind: { strength: 0.3, frequency: 1.2, height: 1 },});// The scatter rides on the terrain's own entity, so it reads its height field directly.const grass = ground.addComponent(TerrainScatter, {  mesh: MeshAsset.plane(app, { width: 0.6, height: 0.8 }),  material: grassMaterial,  density: 2,  // Only where the splat says grass, and only on ground a blade could grow on.  layers: ["grass"],  layerThreshold: 0.5,  slope: { x: 0, y: 25 },  height: { x: 1, y: 45 },  scale: { x: 0.8, y: 1.3 },  randomYaw: true,  alignToNormal: false,  seed: 4,  // Sizes the instance buffer: raising it after `app.start()` is refused with IGX-0717.  maxInstances: 20_000,});await app.start();app.log.info("blades placed:", grass.count);// A density slider changes the rule and then asks for the placement again; it is not per-frame work.grass.density = 4;grass.regenerate();app.log.info("after the density change:", grass.count);

Source: examples/recipes/scatter-foliage/main.ts