js/grass.js: reusable single-file grass module for all our games — GPU-instanced blades (33k+ in one draw call), vertex-shader wind hashed per blade, root->tip fake-AO gradient x per-instance tint, alpha-test pixel style (nearest-filter 16x24 canvas cards, stepped sway), deterministic seed, player-push uniform. docs/GRASS.md records the technique ladder (clump maps, shell texturing, billboard LOD, MB atlas gen) for future tiers. Auto-grows on parkland + islands, PARK panel style/density, included in exported levels via buildParkScene. Test ride: grind physics — falling onto any flattened steel (rails, ledge caps, pool coping) locks a slide along the segment, pop off with Space or ride off the end; grass parts around the rider. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.2 KiB
3.2 KiB
GODVERSE grass — the module and the tricks
js/grass.js is the canonical grass system for all our games. It's a system, not a
mesh — that's why it lives as a code module with style presets, not as GLBs on the
farm. Lift the single file into any three.js project:
import { makeGrass } from './grass.js';
const grass = makeGrass({ heightAt, areas, style: 'lush', density: 8 });
scene.add(grass); // self-ticking wind; add and forget
grass.userData.setPlayer(x, z); // optional: grass parts around the player
Styles: lush (tapered instanced blades), dry (same, straw palette),
pixel (crossed alpha-test cards, 16×24 nearest-filter canvas texture, banded
palette, stepped sway — PS1/N64 vibe).
The tricks it uses (why it's fast)
- GPU instancing — one
InstancedMesh= one draw call for up to 40k blades. Per-blade cost after spawn is zero on the CPU. - Vertex-shader wind —
sin(time + hash(blade origin)), bend scaled byuv.y²so roots stay planted, tips fly. Two frequencies layered so it doesn't look like a metronome. Pixel style floors the sway to 3 steps. - Root→tip gradient with darkened base — geometry vertex colors (0.35 at the root → 1.15 at the tip) multiply with a per-instance tip tint. Reads as ambient occlusion without any lighting cost. The single biggest visual win.
- Per-instance jitter — scale, rotation, lean, hue offset. Uniformity is what makes cheap grass look cheap.
- alphaTest, never alpha-blend — no depth sorting, no overdraw melt.
- No per-blade shadows — the base darkening does that job.
- Deterministic seed (mulberry32) — same park, same lawn, every load.
- Player-push uniform — blades within 1.2m bend away from the rider. Sells physicality for one uniform update per frame.
The next tiers (when a game needs bigger fields)
- Chunked frustum culling — split into 16m tiles, cull per-tile. Needed beyond ~100m fields; our parks don't need it yet.
- Far-field billboard cards — crossed quads with a grass-clump texture past ~40m, instanced blades near. Classic LOD split.
- Clump maps (Ghost of Tsushima GDC 2021, "Procedural Grass in Ghost of Tsushima") — sample a Voronoi clump texture: blades in a clump share lean/height, clump centers get taller blades. Breaks the "even lawn" look at scale.
- Shell texturing (Acerola's "How Do Games Render So Much Grass?") — N stacked transparent shells; dot patterns at increasing height thresholds. Ultra cheap retro turf/fur; a natural fourth style for the pixel games.
- Bezier blades + tessellation for hero close-ups (Sucker Punch use 3-4 verts per blade too — verts are not where the money is; shading is).
- MODELBEAST atlas textures — flux_local can generate blade/clump atlases for
the billboard tiers (
grass blade atlas, alpha background, hand-painted style), thenbg_remove_localfor clean alpha.
In SKATEMAKER PRO
Grass auto-grows on the parkland ring outside the slab and on ISLAND pads, from
park.grass ({style, density} — PARK panel when nothing is selected). Exported
levels include it via buildParkScene; the editor rebuilds it on committed ground
changes only (never mid-drag).