NatureGL Grassv1.1.0

Reference

Quality levels

Four tiers set the ring sizes, blades per chunk, segments per blade and the trample resolution. Switching tiers only rebuilds the rings, so you can change it at runtime.

js
const grass = await GrassSystem.create({ renderer, scene, camera, terrain, quality: 'medium' });
grass.setQualityLevel('ultra');
grass.quality;   // 'ultra'
blades drawn on low
~0.13 M
on medium
~0.26 M
on high
~0.48 M
on ultra
~0.96 M

The blade counts were measured in the demo's meadow view at 1280×720, after frustum culling. They are from the package README.

#The tiers

Each ring lists blades per chunk side, segments per blade and its outer radius. The chunks are 8 m, 16 m and 32 m in every tier.

TierNear ringMid ringFar ringGrass reachesTrample
low64² · 4 seg · 18 m64² · 2 seg · 40 m64² · 1 seg · 70 m70 m512²
medium84² · 5 seg · 22 m80² · 3 seg · 48 m80² · 2 seg · 90 m90 m768²
high104² · 6 seg · 26 m100² · 3 seg · 56 m100² · 2 seg · 110 m110 m1024²
ultra124² · 7 seg · 32 m120² · 4 seg · 68 m120² · 2 seg · 140 m140 m1024²

A blade with n segments has 2n + 1 vertices, so a near blade on high has 13. Blade widths per ring are 0.07 / 0.16 / 0.3 m on low, 0.06 / 0.12 / 0.22 m on medium, 0.055 / 0.1 / 0.18 m on high and 0.05 / 0.085 / 0.15 m on ultra. grass.width multiplies them all.

#Adaptive quality

The library does not change tiers on its own. The demo shows one way to do it: measure the first seconds and step down once.

demo/main.js (simplified)
// average fps over seconds 1.5–5.5, then step down one tier if it is under 45
const order = Object.keys(QUALITY_LEVELS);          // ['low', 'medium', 'high', 'ultra']
const i = order.indexOf(grass.quality);
if (avgFps < 45 && i > 0) grass.setQualityLevel(order[i - 1]);

#Custom tiers

QUALITY_LEVELS is exported and mutable. Change a tier before create(), or add your own key:

js
import { QUALITY_LEVELS } from 'naturegl-grass';

QUALITY_LEVELS.mobile = {
  label: 'MOBILE',
  lods: [
    { chunk: 8, blades: 56, segs: 3, width: 0.08, radius: 16 },
    { chunk: 16, blades: 56, segs: 2, width: 0.18, radius: 36 },
    { chunk: 32, blades: 56, segs: 1, width: 0.32, radius: 60 },
  ],
  trampleResolution: 512,
  clumpSearch: 1,
};
grass.setQualityLevel('mobile');
Ring field
chunkChunk edge in metres
bladesBlades per chunk side. A chunk holds blades²
segsSegments per blade
widthBlade width in metres. Wider far blades keep the coverage
radiusOuter radius of the ring, from the camera

#Beyond the tiers

  • Density (setParams({ grass: { density } })) thins blades inside the same rings. Thinned blades exit the blade pass early and collapse to zero height, so they cost almost no shading, but the instance count stays the same.
  • grass.enabled = false hides the grass and skips the culling and blade pass entirely. Wind and trampling keep running.
  • Shadows: the grass only samples the shadow map. The cost of rendering it is your casters'.