Guides
Chunked LOD tiles
The grass is not a fixed patch. Three rings of square chunks follow the camera, from dense full blades at your feet to wide low-poly blades at the edge. Only chunks inside the view are drawn, with one instanced draw per ring.
meadow at 18 m. The rings move with the camera, and past the last ring the blades have already blended into the turf colour painted on the ground.#Three rings
Each quality tier defines three rings, nearest first. A ring is a grid of square chunks, and each chunk holds a fixed grid of jittered blade positions.
| Ring | Chunk | Blades per chunk (high) | Segments (high) | Outer radius (high) |
|---|---|---|---|---|
| Near | 8 m | 104² = 10,816 | 6 | 26 m |
| Mid | 16 m | 100² = 10,000 | 3 | 56 m |
| Far | 32 m | 100² = 10,000 | 2 | 110 m |
Blade spacing doubles with each ring (8 m / 104 vs 32 m / 100), and the far rings use wider blades (0.055 m, 0.1 m and 0.18 m on high) so the coverage holds. The blade strip has 2 × segments + 1 vertices: 13 near, 5 far.
#What happens in update()
#Pick the chunks
For each ring, every chunk that overlaps the ring's radius around the camera is a candidate. Chunks that sit entirely inside the previous ring (closer than 72 % of its radius) are skipped.
#Cull them on the CPU
The chunk's height range comes from the baked terrain (rectStats, cached per chunk), plus the tallest blade. Chunks whose maxMask is 0 or whose box is outside the camera frustum are dropped.
#Sort front to back
The survivors are sorted by distance, so the early depth test rejects hidden blades. The grass meshes also draw before your ground (renderOrder −10 and up), and the ground is rejected under dense grass.
#Run the blade pass
One fragment per blade computes placement, clumps, wind, trampling and colour factors into four float render targets. The blade vertex shader then only fetches four texels.
#Draw
Each ring is one Mesh with an InstancedBufferGeometry, and its instanceCount is the visible chunks × blades per chunk.
#Ring hand-off
Neighbouring rings overlap. Over the band from 72 % of a ring's inner radius to its inner radius, the new ring thins in while the previous one thins out, so there is no seam. The last ring does not thin out. Its blades shrink into the ground from 62 % of its outer radius to the edge.
#No visible ring
Blades also blend their colour into a turf colour with distance. On high this runs from 28 % to 72 % of the outer radius (31 m to 79 m). patchTerrainMaterial() paints the same turf colour on your ground, using the same palette, dry patches and gust sheen. Past the last ring you see ground that already looks like the far grass.
grass.patchTerrainMaterial(groundMat); // MeshStandard, Physical, Lambert or Phong
grass.patchTerrainMaterial(groundMat, { amount: 0.85 }); // NatureGL Island keeps a little soil showing

meadow on low (grass ends at 70 m) with the fog thinned, turf amount 0 and 1. Without the patch, soil shows between the thinner far blades and past the last ring.#Read the numbers
grass.stats; // { chunks, blades, drawCalls, lods: [{ chunks, blades }, …] } for the last update()
grass.bladeCount; // same as stats.blades
grass.maxDistance; // outer radius of the last ring, in metresThe demo's HUD shows stats.blades, stats.chunks and the near ring's segment count.
#Tune the rings
QUALITY_LEVELS is a plain object. Edit it before create(), or switch tiers at runtime with setQualityLevel(), which rebuilds the rings cheaply.
import { QUALITY_LEVELS, GrassSystem } from 'naturegl-grass';
QUALITY_LEVELS.high.lods[2].radius = 140; // push the far ring out
const grass = await GrassSystem.create({ renderer, scene, camera, terrain, quality: 'high' });Each ring's chunk list and blade data are sized from radius / chunk when the tier is built, so edited rings get buffers that fit.