Three.js Water: How to Render Realistic Water

Three.js water is a flat plane whose surface normal is perturbed by a scrolling or animated normal map (or procedural noise) to fake waves, combined with a reflection render pass. Three.js ships a ready-made Water addon that does exactly this, and it is the fastest way to get a convincing ocean or lake on screen.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The technique: a plane, a normal map, and a reflection

Water rendering in real time is an illusion built from three layers stacked on a flat plane:

  1. Wave normals. A scrolling or time-animated normal map (or a procedural noise function) perturbs the surface normal per-pixel, so lighting reacts as if the surface has ripples, without a single extra vertex.
  2. Reflection. A second camera renders the scene mirrored across the water plane into a texture, sampled and distorted by the same wave normals. This is what makes water read as water rather than a tinted plane.
  3. Refraction and depth-based tinting. Sampling what's under the water (a depth pass or a render of the scene without the water plane) and tinting it darker with distance sells depth: shallow water near shore looks different from the deep end.

Three.js ships a Water addon (three/examples/jsm/objects/Water.js) implementing steps 1 and 2 out of the box. For most projects, start there before reaching for a fully custom shader.

The built-in Water class vs a custom TSL shader

The built-in Water class is the pragmatic default: drop it in, feed it a normal map (the addon ships with a usable one), tune a few uniforms (distortion scale, wave speed, water color), and you have a reflective animated surface in minutes.

A custom shader (GLSL ShaderMaterial or TSL node material) is worth it when you need: stylized non-photorealistic water (toon shading, painterly foam), interaction (ripples from objects entering the water, computed in a compute shader), or refraction alongside reflection for genuinely transparent shallow water. The stock addon does reflection only.

Performance notes

The reflection pass is the expensive part: it's a second full scene render every frame. Common mitigations: render the reflection at a lower resolution than the main view (it's usually blurred by the wave distortion anyway, so detail loss is hard to notice), cull objects unlikely to be visible in the reflection, and skip the reflection pass entirely when the water is off-screen or far from the camera.

Tools and libraries

Code

Water addon (r181)
1import { Water } from 'three/examples/jsm/objects/Water.js'
2import * as THREE from 'three'
3
4const waterGeometry = new THREE.PlaneGeometry(200, 200)
5const water = new Water(waterGeometry, {
6 textureWidth: 512,
7 textureHeight: 512,
8 waterNormals: new THREE.TextureLoader().load('/waternormals.jpg', (t) => {
9 t.wrapS = t.wrapT = THREE.RepeatWrapping
10 }),
11 sunDirection: new THREE.Vector3(),
12 sunColor: 0xffffff,
13 waterColor: 0x001e3f,
14 distortionScale: 3.7,
15})
16water.rotation.x = -Math.PI / 2
17scene.add(water)
18
19function animate() {
20 water.material.uniforms['time'].value += 1 / 60
21 renderer.render(scene, camera)
22}
TSL wave normals (r181)
1import { texture, uv, timerLocal, vec2, normalMap } from 'three/tsl'
2
3const waterNormalTex = texture(normalMapTexture)
4const scroll1 = uv().add(vec2(timerLocal().mul(0.02), 0))
5const scroll2 = uv().add(vec2(0, timerLocal().mul(0.015)))
6
7const material = new THREE.MeshStandardNodeMaterial()
8material.normalNode = normalMap(waterNormalTex.sample(scroll1))
9// combine a second scrolling sample for a less repetitive ripple pattern

Learn this properly

Learn Practical TSL

Your First Node Material

Node materials are the foundation for building custom water shaders in TSL.

Start the lesson (6 minutes)

Frequently asked questions

How do you make water in Three.js?

The fastest path is the built-in Water addon (three/examples/jsm/objects/Water.js): a flat plane with animated wave normals and a real-time reflection pass. For stylized or interactive water, write a custom ShaderMaterial or TSL node material instead.

Why is my water reflection slow?

The reflection is a second full scene render every frame. Lower the reflection render target resolution, cull objects unlikely to appear in the reflection, and skip the pass when the water is off-screen.

Does Three.js water support refraction as well as reflection?

The built-in Water addon only does reflection. Refraction (seeing distorted objects through the water) requires a custom shader that samples a render of the scene without the water plane.

Keep reading