NatureGL Grassv1.1.0

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

js
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.

MethodDefaultsWhat you get
addFlowers({ amount, height, palette, viewDistance, stemColor })amount 0.35, view 42 mPatchy 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 mThree 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.6Three 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.8Leafy 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:

demo/main.js
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.

js
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,
});
OptionDefault
density0.01Instances per m², random placement
positions—Explicit placements instead
chunkSize32Streaming chunk size in metres
viewDistance150Chunks are built within this radius of the camera
fadetrueShrink instances into the ground from 72 % to 98 % of viewDistance
scale[0.8, 1.2]Random uniform scale range
sink0Push into the ground by this × scale
maskPower1The 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
seed1Per-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 buildBudget per update, and freed once they are farther than viewDistance × 1.2 plus a chunk.
  • Each part of each chunk is its own InstancedMesh.

#Change a layer

js
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 layers

scatter.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.