Guides
Trees, flowers and rocks
ScatterSystem streams instanced props over the same terrain as the grass. Placement is deterministic per chunk, props shrink away at their view distance instead of popping, and they share the grass's wind and trampling.
wildflowers: the flower layer at amount 1.0, twelve placed trees and eighteen placed rocks. The rocks' footprints are also in the grass mask.#Create it from the grass
import { GrassSystem, ScatterSystem } from 'naturegl-grass';
const grass = await GrassSystem.create({ renderer, scene, camera, terrain });
const scatter = await ScatterSystem.create({ scene, camera, grass }); // reuses grass.terrain, .wind, .trample
const flowers = scatter.addFlowers({ amount: 0.3 });
const trees = scatter.addTrees({ density: 0.0006 });
const rocks = scatter.addRocks({ density: 0.004 });
const bushes = scatter.addBushes({ density: 0.004 });
renderer.setAnimationLoop(() => {
grass.update(dt);
scatter.update(dt); // streams chunks around the camera
renderer.render(scene, camera);
});Without a grass system, pass { scene, camera, terrain, wind?, trample?, seed?, buildBudget? }. A plain terrain description is baked at 512² for the scatter. buildBudget (default 6) caps how many chunks are built per update(), which spreads the CPU cost.
#Ready-made layers
Each call returns a ScatterLayer.
| Method | Defaults | What you get |
|---|---|---|
addFlowers({ amount, height, palette, viewDistance, stemColor }) | amount 0.35, view 42 m | Patchy wildflowers with a palette colour per instance. They bend in the wind and flatten under tramplers. Adds layer.material, layer.stemColor and layer.setAmount(a) |
addRocks({ density, positions, scale, color, viewDistance }) | 0.004 /m², view 160 m | Three lumpy rock shapes with baked AO and a speckle texture. Adds layer.material |
addTrees({ density, positions, scale, leafColor, viewDistance }) | 0.0006 /m², view 320 m, max slope 0.6 | Three procedural broadleaf trees with wind sway, back-lit leaves and alpha-tested shadows. Adds layer.barkMaterial and layer.leafMaterial |
addBushes({ density, positions, scale, color, viewDistance }) | 0.004 /m², view 120 m, max slope 0.8 | Leafy bushes that sway and flatten. Adds layer.material |
#Place props by hand
Pass positions instead of density. The demo's trees are placed this way:
const TREE_SPOTS = [[14, -10, 1.2], [-18, -16, 1.0], [26, 12, 1.35] /* … */];
const trees = scatter.addTrees({
positions: TREE_SPOTS.map(([x, z, s], i) => ({ x, z, scale: s, variant: i % 3 })),
});
trees.barkMaterial.color.multiplyScalar(0.5);A placement is { x, z, scale?, scaleY?, rotation?, variant? }. Placed props are built once as a single chunk. They skip the mask test, and they don't fade. The savanna preset squashes the same trees into flat acacias with scaleY.
#Custom layers
addLayer() scatters any geometry and material. A variant is a list of parts drawn together, such as bark and leaves.
const mushrooms = scatter.addLayer({
name: 'mushrooms',
variants: [[{ geometry: capGeo, material: capMat }, { geometry: stemGeo, material: stemMat }]],
density: 0.05, // instances per m²
chunkSize: 16, viewDistance: 60, fade: true,
scale: [0.8, 1.2], sink: 0,
maskPower: 1, // keep probability = mask ^ maskPower (0 ignores the mask)
maxSlope: 0.5, // rise / run
cluster: { scale: 0.08, amount: 0.5 }, // noise patches
filter: (x, z, y, rand) => y > 2,
color: (i, rand, color) => color.setHSL(rand(), 0.5, 0.5), // per-instance colour
seed: 3,
});| Option | Default | |
|---|---|---|
density | 0.01 | Instances per m², random placement |
positions | — | Explicit placements instead |
chunkSize | 32 | Streaming chunk size in metres |
viewDistance | 150 | Chunks are built within this radius of the camera |
fade | true | Shrink instances into the ground from 72 % to 98 % of viewDistance |
scale | [0.8, 1.2] | Random uniform scale range |
sink | 0 | Push into the ground by this × scale |
maskPower | 1 | The terrain mask raised to this power is the keep probability |
maxSlope | — | Reject slopes steeper than this (rise over run) |
cluster | — | { scale, amount }: keep only where a noise field is high |
filter | — | (x, z, y, rand) => boolean |
color | — | (i, rand, color) => void, sets instanceColor |
seed | 1 | Per-layer seed, combined with the system's seed |
Parts take castShadow and receiveShadow (both default true) and an optional depthMaterial for alpha-tested or wind-patched shadows. To make your own parts sway, patch their materials with grass.wind.patchMaterial() before adding the layer.
#Streaming
- Chunks are deterministic per chunk index and seed, so an area you return to looks the same.
- They are built nearest first, up to
buildBudgetper update, and freed once they are farther thanviewDistance × 1.2plus a chunk. - Each part of each chunk is its own
InstancedMesh.
#Change a layer
flowers.setAmount(1.0); // 0 hides the layer
trees.set({ positions: newSpots }); // any options; rebuilds
rocks.visible = false;
scatter.removeLayer(bushes);
scatter.count; // instances alive across all layersscatter.refresh() rebuilds every layer. scatter.setTerrain(desc?) re-bakes a terrain the scatter owns. A shared grass terrain is picked up on its own after grass.setTerrain().
patchLeafTranslucency(material) is exported too. It adds a back-lit glow from the scene's first directional light to any lit material.