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.
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.
| Tier | Near ring | Mid ring | Far ring | Grass reaches | Trample |
|---|---|---|---|---|---|
low | 64² · 4 seg · 18 m | 64² · 2 seg · 40 m | 64² · 1 seg · 70 m | 70 m | 512² |
medium | 84² · 5 seg · 22 m | 80² · 3 seg · 48 m | 80² · 2 seg · 90 m | 90 m | 768² |
high | 104² · 6 seg · 26 m | 100² · 3 seg · 56 m | 100² · 2 seg · 110 m | 110 m | 1024² |
ultra | 124² · 7 seg · 32 m | 120² · 4 seg · 68 m | 120² · 2 seg · 140 m | 140 m | 1024² |
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.
low: 64² blades per near chunk, 4 segmentsultra: 124² blades per near chunk, 7 segments#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.
// 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:
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 | |
|---|---|
chunk | Chunk edge in metres |
blades | Blades per chunk side. A chunk holds blades² |
segs | Segments per blade |
width | Blade width in metres. Wider far blades keep the coverage |
radius | Outer 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 = falsehides 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'.