Stylized animated grass and water shaders for Three.js, extracted from a small browser game I've been working on. Drop them into your own scene with two lines of code, or pull just the raw GLSL strings.
Built for
three@^0.170and modern WebGL2 (GLSL3).
git clone https://github.com/boona13/threejs-grass-water-shaders.git
cd threejs-grass-water-shaders
npm install
npm run devThe demo (src/demo/main.ts) renders both shaders on a hilly terrain with a
pond. Drag to orbit, scroll to zoom, click on the water to spawn ripples, and
move the cursor over the grass to push the blades aside.
npm run build # type-check + production bundle
npm run preview # serve the production bundlesrc/
grass/
GrassField.ts # Drop-in InstancedMesh of animated blades
grassShader.glsl.ts # Vertex + fragment shaders (GLSL3)
windNoise.glsl.ts # Wang-hash wind lattice noise
index.ts
water/
WaterPlane.ts # Drop-in stylized water surface
WaterMask.ts # Paint pond/river shapes into a float texture
WaterNoiseLUT.ts # Procedural noise LUT (no asset required)
waterShader.glsl.ts # Vertex + fragment shaders (GLSL1)
index.ts
demo/
main.ts # Vite entry: orbit cam, sun, GUI, raycast picking
createTerrain.ts # Procedural hilly terrain + heightmap baker
public/textures/water/
blue_noise.png # Optional: drop-in alternative to the procedural LUT
import * as THREE from 'three';
import { GrassField } from 'threejs-grass-water-shaders/grass';
const grass = new GrassField(scene, {
groundSize: 44, // square area edge length
spacing: 0.18, // blade-to-blade distance (smaller = denser)
bladeHeight: 0.55,
baseColor: new THREE.Color('#7da653'),
tipColor: new THREE.Color('#dee884'),
windSpeed: 1.1,
});
// Optional: conform blades to a heightmap (R32F texture).
grass.setTerrainHeightTexture(heightTex, worldSize, resolution);
// Optional: push blades aside under the player's feet.
grass.setPushField(new THREE.Vector2(player.x, player.z), 1.4, 0.32);
function tick(t: number) {
grass.update(t); // advance time uniform
}- Wind: a Wang-hash bilinear lattice driven by
time * windSpeed, with a second faster octave (gustStrength) that adds high-frequency rustle. - Blade bend: per-tuft forward arc (
bendStrength) weighted by tip position, damped by camera distance. - Push field: a soft circular field that flattens and pushes nearby tips outward — drive it from your character controller for footstep ripples.
- Slope cull: blades are clipped where the heightmap slope exceeds
0.65, with a smooth shoulder of stochastic thinning between0.28..0.65so the field fades naturally onto cliffs and rocks. - Grow-in: each tuft has a
birthTimeattribute and animates from zero-scale overgrowthDurationseconds — useful for spawning on demand. - Three-light shading: directional sun + hemisphere ambient + opposite fill, plus a tip-color additive lift and a Three.js shadow map factor.
The default config places ~30k blades over a 44×44 m square at spacing=0.18.
With segments=4 × bladesPerTuft=3 that's ~24 triangles per tuft. On an
M-series Mac this renders in <1 ms. For a far-LOD pass, build a second
InstancedMesh with cheaper geometry (e.g. segments=2, bladesPerTuft=2)
that shares the same material and only contains tufts beyond a distance
threshold.
import * as THREE from 'three';
import { WaterPlane } from 'threejs-grass-water-shaders/water';
const water = new WaterPlane({
size: 80,
deepColor: new THREE.Color('#1f5680'),
shallowColor: new THREE.Color('#7cc6e8'),
skyLowColor: new THREE.Color(0.72, 0.84, 0.95),
skyHighColor: new THREE.Color(0.92, 0.97, 1.0),
});
water.mask.paintCircle(0, 0, 12); // pond
water.mask.paintCapsule(0, -8, 14, -22, 1.6); // river
water.mesh.position.y = -0.12; // pond surface elevation
scene.add(water.mesh);
// Optional: bind a heightmap so streams/rivers dip into valleys
// water.setTerrainHeightTexture(heightTex, 80);
renderer.domElement.addEventListener('pointerdown', (e) => {
const p = pickWater(e); // raycast against water.mesh
if (p) water.spawnRipple(p.x, p.z, performance.now() / 1000);
});
function tick(t: number) {
water.update(t);
}- 4-octave animated normals sampled from a procedural noise LUT
(
WaterNoiseLUT). Each octave has its own scale, amplitude, rotation, and flow direction (scales = [2, 4, 8, 12],amps = [2, 2, 0.5, 0.2]). - Sky reflection: Schlick Fresnel against a procedural sky gradient
(
skyLowColor→skyHighColor). Stands in for a real SSR pass. - Ripples: up to 32 expanding sine envelopes, each fading over ~1.8 s.
Spawn them with
water.spawnRipple(x, z, time). - Foam: up to 48 procedural intersection rings (e.g. where rocks pierce
the surface). Feed flat
[x, z, radius]triplets viasetFoamSources. - Painted mask:
WaterMaskexposespaintCircle/paintCapsulefor authoring pond/river shapes into a float texture. The shader adds a noise band to the mask edge for a soft, natural shoreline. - Specular highlight: Blinn-Phong on the animated normal.
- Shadow map factor: respects the scene's
DirectionalLightshadow.
All visually meaningful parameters are real uniforms — bind a lil-gui (the
demo does) and tweak live. The most useful sliders:
| Uniform | Effect |
|---|---|
flowSpeed |
UV scroll rate of the noise octaves |
rippleBoost |
Multiplier on the gradient — bigger = choppier |
reflectionStrength |
Forces extra reflection beyond pure Fresnel |
shoreGlow |
Brightening near shorelines |
specularPower |
Tightness of the sun highlight |
specularIntensity |
Brightness of the sun highlight |
If you want to use the shaders directly without the wrapper classes, every shader source is exported as a string:
import { GRASS_VERTEX, GRASS_FRAGMENT } from 'threejs-grass-water-shaders/grass';
import { WATER_VERTEX, WATER_FRAGMENT } from 'threejs-grass-water-shaders/water';Wire your own uniforms / instanced attributes — the names are documented at
the top of each *.glsl.ts file.
The shipped shaders cover the visually defining features. If you want to push further, the most common additions in a real game are:
- Path / wall exclusion field: paint a 0..1 mask texture over your world
and have the grass vertex shader sample it to clip blades inside built-up
areas. The
setExclusionMaskTexture()-style hook is a one-uniform addition. - LOD with two
InstancedMeshzones: build a second mesh with cheaper geometry (segments=2,bladesPerTuft=2) for tufts beyond ~14 m and skip shadow casting on it. - Multi-light contributions: extend the fragment shader with arrays of point-light positions / colors / radii to pick up campfires or window glows.
- Real blue-noise LUT: drop
public/textures/water/blue_noise.pnginto a customWaterNoiseLUTsubclass that runs the bicubic-B-spline + central- difference pass on it for crisper, lower-banding ripples. - Snow / ice transition: add a
uSnowMixuniform; in the water shader, blend toward an ice-color palette plus a Voronoi crack pattern. - Animal / NPC push field: extend the push uniform from a single
vec2 + radiusto an array of up to N positions, looped in the vertex shader.
MIT — do whatever you want with the code, attribution appreciated but not required.
