More
Under the hood
What the grass does each frame, and what changed from the single-file grasspro.html page it grew from. Drag each slider to compare the two from the same camera and preset.
#The frame
#Wind and trample
wind.update(dt) advances the wind time. The trample pass decays last frame's flattening, scrolls the buffer if the camera has moved far enough, and applies up to 16 stamps.
#Chunk lists
For each of the three rings, the CPU gathers the chunks in range and drops those that are fully masked or outside the frustum. It uses the baked height range of each chunk, cached. The survivors are sorted front to back and written to a small float texture of chunk origins.
#Blade pass
One fullscreen draw per ring into four float render targets (MRT), one texel per blade. Each fragment works out everything that is constant along its blade:
| Target | Holds |
|---|---|
| A | Position, ground height, blade height (≤ 0 = culled) |
| B | Bend from wind, lean, clumping and trampling; facing angle; width |
| C | Colour multiplier; packed root AO, dryness and trodden amount |
| D | Far turf colour; packed far blend, dry tip and gust strength |
#Blade draw
Each ring is one instanced draw. The blade vertex shader fetches its four texels and bends a strip of 2 × segments + 1 vertices along an arc. It no longer runs noise, clump searches or wind per vertex.
#Shading
Hemispherical ambient with root AO, the sun with the PCF shadow map and the optional cloud shadow, back-lit translucency, a Kajiya-Kay sheen along the blade, gust sheen, and scene fog.
#No grass ring
The original drew two camera-centred grids that ended about 50 m out on pale bare ground. The new rings reach 110 m on high. The far blades blend into a calibrated turf colour, and patchTerrainMaterial() paints the same colour on the ground, so the field carries on to the horizon.


#More visible blades for the cost
The original ran the full vertex shader for 0.6 M instances every frame, including all the off-screen ones. Now:
- the CPU culls whole chunks against the frustum and the view distance,
- chunks are drawn front to back, so hidden blades fail the depth test early,
- and the per-blade work runs once per blade in the blade pass, not once per vertex.
#A better blade
- Clumping. Voronoi tufts share height and tint, and their blades splay outward from the tuft centre.
- Dry tips on some blades, and dry patches across the field.
- Root AO. Darker at the base where the sward is denser.
- Anisotropic sheen. A Kajiya-Kay highlight along the blade.
- Gust sheen. Bent blades show their paler side, so gust fronts read as waves of light.
- Translucency. Reworked into light on the far face plus forward scatter.

golden: back-lit blades, same camera.#Everything else that changed
| Area | NatureGL Grass |
|---|---|
| Terrain | Was hard-coded in both JS and GLSL. Now any heightAt, mask and colorAt, baked once. The demo's Path toggle and the grass-free rock footprints use the mask |
| Lights | three.js lights through bindLights(), including real PCF shadow maps through sampler2DShadow, in physically based light units |
| Sky | An optional cloud-shadow input |
| Scene | Meshes go into the caller's scene |
| Wind | A shared field that other materials adopt. It chains with other onBeforeCompile patches |
| Trampling | Was a fixed ±80 m square. Now follows the camera, with 16 tramplers plus one-shot stamps. Objects that lift off stop flattening |
| Props | ScatterSystem: flowers, rocks, trees and bushes streamed in chunks with distance fade. Flowers use the wind and trampling |
| Tree leaves | Back-lit. The original's leaf glow was front-lit by mistake |
| three.js | 0.160 → 0.186: THREE.Timer replaces Clock, PCFShadowMap replaces the removed PCFSoftShadowMap, DataTextures replace canvas textures, and GL errors fail the tests |