NatureGL Grassv1.1.0

Guides

Terrain

The grass grows on any ground you can describe with a height function. You pass the function, the area and optional mask and tint callbacks. They are baked once into textures that every blade reads.

#The terrain description

ts
{
  heightAt(x: number, z: number): number;                  // required: world height at (x, z)
  bounds: { minX, minZ, maxX, maxZ } | THREE.Box2 | [minX, minZ, maxX, maxZ];
  mask?(x: number, z: number): number;                     // 0..1 coverage; 0 = no grass
  colorAt?(x: number, z: number): THREE.Color | [r, g, b]; // linear tint, 1 = unchanged, up to 2
  resolution?: number;                                     // bake texels per side
}
Field
heightAtCalled once per bake texel. It does not need to be fast, but it must match your ground mesh
boundsThe rectangle to bake. Grass never grows outside it. A THREE.Box2 uses its y as world Z
maskGrass coverage from 0 to 1, clamped. Use it for paths, beaches, water and building footprints
colorAtLinear RGB multiplied into the grass colour, for example yellower grass near a beach
resolutionTexels per side. The default is the larger side divided by 0.9 m, clamped to 256..1024

Pass the description as terrain to GrassSystem.create(). The system keeps the bake as grass.terrain, a TerrainField.

#Carve out paths and water

A mask of 0 removes the blades, and fractional values thin them. The demo's Path toggle is one smoothstep along a sine curve:

demo/main.js
const pathZ = x => 18 * Math.sin(x * 0.035) + 6 * Math.sin(x * 0.09 + 1.0);
const pathMask = (x, z) => THREE.MathUtils.smoothstep(Math.abs(z - pathZ(x)), 0.9, 2.1);

const terrainDesc = () => ({
  heightAt: terrainH, bounds: [-450, -450, 450, 450], resolution: 1024,
  mask: (x, z) => rockMask(x, z) * (pathOn ? pathMask(x, z) : 1),
});

rockMask does the same for the demo's 18 rocks, so no blades grow up through them.

The path mask. The turf colour that patchTerrainMaterial() paints on the ground follows the mask too, so the path shows bare soil.

#Tint by location

js
const terrain = {
  heightAt, bounds: [-300, -300, 300, 300], mask,
  colorAt: (x, z) => {
    const nearBeach = THREE.MathUtils.smoothstep(distanceToShore(x, z), 12, 0);
    return [1 + 0.3 * nearBeach, 1 + 0.15 * nearBeach, 1 - 0.2 * nearBeach];   // yellower by the sand
  },
};

The tint is baked at up to 512² into an 8-bit texture, stored at half value so tints up to 2× fit. distanceToShore is your own function.

#Change the terrain

The bake is a snapshot. When the ground changes, re-bake it:

js
grass.setTerrain(newDescription);

A ScatterSystem or Fireflies built with { grass } shares the same TerrainField. They pick up the new bake automatically: the scatter layers rebuild on their next update() (terrain.version increments), and the fireflies read the shared textures.

#Query heights on the CPU

js
grass.terrain.heightAt(x, z);   // bilinear sample of the bake, fast
grass.terrain.maskAt(x, z);     // 0 outside bounds
grass.terrain.rectStats(x0, z0, x1, z1);   // { minY, maxY, maxMask }

rectStats is what the chunk culling uses. A chunk whose maxMask is 0 is skipped outright.

#Share one bake

TerrainField is exported. Build it once and pass it to several systems:

js
import { TerrainField, GrassSystem, ScatterSystem } from 'naturegl-grass';

const field = new TerrainField({ heightAt, bounds, mask });
const grass = await GrassSystem.create({ renderer, scene, camera, terrain: field });
const scatter = await ScatterSystem.create({ scene, camera, terrain: field, wind: grass.wind, trample: grass.trample });