Introduction
As Web-based 3D experiences get closer to cinematic visuals, one of the biggest challenges developers face is creating realistic water. The default materials in Three.js offer a good starting point, but achieving believable waves, caustics, foam, depth, and color variation requires going beyond built-in tools and working directly with custom shaders.
In this article, we’ll explore how to build an advanced, visually rich water system in Three.js using GLSL shaders, flow maps, caustic textures, and particle-based foam. If you’re already comfortable with basic materials and lighting in Three.js, this guide will help you take the next step toward high-quality real-time water rendering.
Three.js provides tools like THREE.Water and THREE.Water2, but for projects demanding full artistic control—such as XR/VR education, simulations, or games—you need:
- Custom wave animations
- Dynamic color blending based on depth
- Real caustics that move with the waves
- Flow maps for directional water movement
- Foam near edges or colliding objects
- Underwater lighting effects
With shaders, you can precisely control how your water moves, reflects, and interacts with its environment.
1. Building the Base Water Geometry
Start with a high-resolution plane:
const geometry = new THREE.PlaneGeometry(25, 25, 512, 512);
Higher subdivisions allow smoother wave animations through vertex displacement.
Using ShaderMaterial gives full control:
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uBigWaveElevation: { value: 0.18 },
uBigWaveSpeed: { value: 0.6 },
uWaterColor: { value: new THREE.Color('#135fb0') },
uDepthColor: { value: new THREE.Color('#001d3d') }
}
});
2. Adding Wave Motion With GLSL
Using Perlin or Simplex noise, you can create layered waves:
float elevation = sin(position.x * frequency + time * speed) *
sin(position.y * frequency + time * speed) *
amplitude;
Multiple layers = big ocean waves + small ripples.
3. Depth-Based Color Blending
Natural water becomes darker with depth.
By calculating fragment depth, you blend two colors:
float depthFactor = smoothstep(0.0, 1.0, vDepth); vec3 finalColor = mix(uWaterColor, uDepthColor, depthFactor);
This instantly boosts realism.
4. Adding Caustic Effects
Caustics create the signature shimmering light patterns seen in shallow water.
Techniques include:
- Using pre-baked tiling caustic textures
- Animating them with UV scrolling
- Modulating brightness based on wave height
vec2 causticUV = vUv + vec2(time * 0.02, time * 0.015); vec3 caustics = texture2D(uCausticTexture, causticUV).rgb; finalColor += caustics * 0.2;
Now the water feels alive.
5. Implementing Foam Using Particle Systems
Foam appears:
- Around edges
- Where waves break
- Near moving objects
A lightweight approach uses a particle shader:
- Noise-driven dissolve
- Soft alpha falloff
- Additive or soft blending
const points = new THREE.Points(foamGeometry, foamMaterial); scene.add(points);
Simple but very effective.
6. Adding Flow Maps for Directional Movement
Flow maps allow water to move as if following currents.
They distort UV coordinates and create directional motion without heavy physics.
vec2 flow = texture2D(uFlowMap, vUv).rg * 2.0 - 1.0; vec2 uv = vUv + flow * time * 0.05;
You can simulate:
- River currents
- Tide movement
- Circular pool motion
7. Performance Optimization Tips
To keep FPS high:
- Reduce plane subdivisions far from the camera
- Use
THREE.Clockinstead ofperformance.now() - Keep caustic textures small & tileable
- Use GPU-friendly noise (simplex noise)
- Enable mipmapping for textures
- Animate only what is needed (uniform-driven)
Conclusion
Realistic water in Three.js is not about one trick—it’s a combination of shader-based wave motion, depth-based color, caustics, foam, and flow maps. By combining these techniques, you can build ocean surfaces that feel dynamic and natural, whether for game environments, XR simulations, or educational visualizations.
This approach gives you full artistic control and pushes the boundaries of what’s possible inside a web browser.