askill
ta-water-shader

ta-water-shaderSafety 100Repository

Gerstner wave simulation with foam and caustics for realistic water surfaces in Three.js/R3F. Use when creating water shaders, oceans, lakes, or any animated liquid surfaces.

0 stars
1.2k downloads
Updated 2/5/2026

Package Files

Loading files...
SKILL.md

Water Shader Skill

"Realistic water that moves like water."

Overview

Water shaders simulate liquid surfaces using Gerstner waves for realistic wave motion, foam generation at wave peaks, fresnel reflections for depth, and caustics for underwater light patterns.

When to Use This Skill

Use when your task involves:

  • Water surfaces (oceans, lakes, rivers, pools)
  • Wave simulation and animation
  • Foam and spray effects
  • Underwater caustics
  • Fresnel reflections on liquid
  • Buoyancy and floating objects

Core Concepts

Gerstner Waves

Gerstner waves are a more realistic wave model than simple sine waves:

Direction: Waves travel in a specific direction
Steepness: Controls how peaked the waves are (0-1)
Wavelength: Distance between wave crests
Speed: Wave velocity based on physics (sqrt(9.8 / k))

Shader Implementation:

vec3 gerstnerWave(vec3 position, float steepness, float wavelength) {
  float k = 2.0 * PI / wavelength;  // Wave number
  float c = sqrt(9.8 / k);           // Wave speed (physics-based)
  vec2 d = vec2(cos(uWaveDirection), sin(uWaveDirection));
  float f = k * (dot(d, position.xz) - c * uTime * uWaveSpeed);
  float a = steepness / k;           // Wave amplitude

  return vec3(
    d.x * a * cos(f),  // X displacement
    a * sin(f),        // Y displacement (height)
    d.y * a * cos(f)   // Z displacement
  );
}

Layered Waves

Multiple wave layers create complexity:

// Primary waves - large, slow
vec3 wave1 = gerstnerWave(position, 0.25, 15.0);

// Secondary waves - medium
vec3 wave2 = gerstnerWave(position, 0.15, 10.0);

// Tertiary waves - small, fast details
vec3 wave3 = gerstnerWave(position, 0.10, 5.0);

// Combine with different weights
transformed += wave1 + wave2 * 0.5 + wave3 * 0.25;

Foam Generation

Foam appears at wave peaks with noise variation:

// Wave height determines foam
float foamThreshold = uWaveHeight * 0.7;
float foamNoise = noise(worldPosition.xz * 0.05 + uTime * 0.05);

// Smooth threshold for soft edges
float foam = smoothstep(
  foamThreshold - 0.1,
  foamThreshold,
  vWaveHeight + foamNoise * 0.2
);

// Add variation with second noise layer
foam *= smoothstep(0.3, 0.6, noise(uv * 2.0 + 100.0));

// Mix foam color
vec3 color = mix(waterColor, foamColor, foam);

Fresnel Reflections

Fresnel effect makes edges more reflective:

vec3 viewDirection = normalize(cameraPosition - worldPosition);
vec3 normal = normalize(vNormal);

// Fresnel = more reflection at grazing angles
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 3.0);

vec3 reflectionColor = mix(waterColor, skyColor, fresnel * 0.5);

Caustics

Underwater light patterns using layered noise:

vec2 causticUV = uv * 20.0 + uTime * 0.1;
float caustic = noise(causticUV) * noise(causticUV * 1.5 + 50.0);
color += vec3(caustic * 0.05);

Implementation Pattern

TypeScript Class Structure

export class WaterShader {
  private material: THREE.ShaderMaterial;

  public readonly uniforms: {
    uTime: { value: number };
    uWaveDirection: { value: number };
    uWaveSpeed: { value: number };
    uWaveHeight: { value: number };
    uWaveFrequency: { value: number };
    uWaterColor: { value: THREE.Color };
    uFoamColor: { value: THREE.Color };
    uNoiseTexture: { value: THREE.Texture | null };
    uCameraPosition: { value: THREE.Vector3 };
  };

  constructor(config?: WaterShaderConfig) {
    // Initialize uniforms with defaults
    // Create shader material
  }

  public getMaterial(): THREE.Material {
    return this.material;
  }

  public updateTime(time: number): void {
    this.uniforms.uTime.value = time;
  }

  public updateCameraPosition(position: THREE.Vector3): void {
    this.uniforms.uCameraPosition.value.copy(position);
  }

  public setWaveParams(direction: number, speed: number, height: number): void {
    this.uniforms.uWaveDirection.value = direction;
    this.uniforms.uWaveSpeed.value = speed;
    this.uniforms.uWaveHeight.value = height;
  }

  public dispose(): void {
    this.material.dispose();
  }
}

Usage in R3F

import { WaterShader, createWaterPlane } from './components/shaders/WaterShader';

// In your scene component
const waterMesh = createWaterPlane(256, 128, {
  waveSpeed: 1.0,
  waveHeight: 0.3,
  waterColor: 0x1a5276,
  foamColor: 0xecf0f1,
});

// In animation loop
useFrame((state, delta) => {
  updateWaterMesh(waterMesh, delta, state.camera.position);
});

Performance Optimization

Geometry Segments

Use appropriate segment count for wave displacement:

// Good balance for 256m x 256m water
const geometry = new THREE.PlaneGeometry(256, 256, 128, 128);

// Lower segments for distant water
const distantWater = new THREE.PlaneGeometry(256, 256, 32, 32);

LOD Strategy

// Distance-based wave detail reduction
float dist = length(worldPosition.xz - cameraPosition.xz);
float lodFactor = smoothstep(50.0, 200.0, dist);

// Reduce wave height at distance
transformed *= (1.0 - lodFactor * 0.5);

Optimization Tips

  1. Shared noise texture - Use single noise texture for all water
  2. Lower resolution for caustics - Don't need full res
  3. Cull distant water - Use frustum culling aggressively
  4. Reuse geometry - Same plane geometry for all water instances

GDD Specifications

From docs/design/gdd/:

PropertyValue
Map Size256m x 256m
Water Levely = 2m
Wave DirectionPI * 0.25 (diagonal)
Wave Speed1.0 (adjustable)
Max Wave Height0.3m
Water ColorDeep blue (#1a5276)
Foam ColorWhite (#ecf0f1)

Asset References

Use existing textures from src/assets/:

  • Pattern Pack - Water surface patterns
  • Noise textures - For foam/caustic variation

Debug Visualization

Add wireframe helper to see wave displacement:

const wireframe = new THREE.WireframeGeometry(waterMesh.geometry);
const line = new THREE.LineSegments(wireframe);
line.position.copy(waterMesh.position);
scene.add(line);

Common Issues

Waves Too Sharp

Reduce steepness in gerstnerWave function:

// Before: 0.25 (sharp peaks)
vec3 wave1 = gerstnerWave(position, 0.25, 15.0);

// After: 0.15 (softer waves)
vec3 wave1 = gerstnerWave(position, 0.15, 15.0);

Foam Looks Static

Add time-based animation to noise UVs:

vec2 noiseUV = worldPosition.xz * 0.05 + uTime * 0.05;

No Reflections

Check normal calculation - normals must be recalculated after wave displacement:

// Finite difference method for normals
float eps = 0.01;
vec3 p1 = position + gerstnerWave(position + vec3(eps, 0.0, 0.0), ...);
vec3 p2 = position + gerstnerWave(position + vec3(0.0, 0.0, eps), ...);

vec3 normal = normalize(vec3(
  p1.x - transformed.x,
  eps,
  p2.z - transformed.z
));

Related Skills

For general shader patterns: Skill("ta-shader-development") For vegetation placement near water: Skill("ta-foliage-instancing") For water-based paint mechanics: Skill("ta-paint-territory")

External References

  • Implementation plan: docs/implementation/terrain-refactor-plan.md (Phase 2)
  • Research guide: docs/research/terrain-shader-research.md
  • Color palette: docs/references/terrain/color-palette.ts
  • Visual reference: docs/references/terrain/README.md
  • ShaderToy Seascape: https://www.shadertoy.com/view/Ms2SD1
  • Three.js Journey Raging Sea

Install

Download ZIP
Requires askill CLI v1.0+

AI Quality Score

95/100Analyzed 2/10/2026

A comprehensive and highly actionable guide for implementing Gerstner wave water shaders in Three.js/R3F, including math, GLSL, and TypeScript patterns.

100
95
85
95
95

Metadata

Licenseunknown
Version-
Updated2/5/2026
Publishermajiayu000

Tags

No tags yet.