Skip to content

boona13/threejs-grass-water-shaders

Repository files navigation

threejs-grass-water-shaders

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.170 and modern WebGL2 (GLSL3).

Preview — characters wading in a pond surrounded by lush animated grass


Demo

git clone https://github.com/boona13/threejs-grass-water-shaders.git
cd threejs-grass-water-shaders
npm install
npm run dev

The 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 bundle

What's inside

src/
  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

Quick start: GrassField

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
}

What the shader does

  • 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 between 0.28..0.65 so the field fades naturally onto cliffs and rocks.
  • Grow-in: each tuft has a birthTime attribute and animates from zero-scale over growthDuration seconds — 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.

Performance

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.


Quick start: WaterPlane

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);
}

What the shader does

  • 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 (skyLowColorskyHighColor). 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 via setFoamSources.
  • Painted mask: WaterMask exposes paintCircle / paintCapsule for 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 DirectionalLight shadow.

Customising

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

Standalone GLSL

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.


Extending

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 InstancedMesh zones: 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.png into a custom WaterNoiseLUT subclass that runs the bicubic-B-spline + central- difference pass on it for crisper, lower-banding ripples.
  • Snow / ice transition: add a uSnowMix uniform; 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 + radius to an array of up to N positions, looped in the vertex shader.

License

MIT — do whatever you want with the code, attribution appreciated but not required.

About

Stylized animated grass and water shaders for Three.js . Drop-in classes + raw GLSL + a runnable demo.

Topics

Resources

License

Stars

10 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors