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.
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:
- 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.
- 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.
- 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
Learn this properly
Learn Practical TSL
Your First Node Material
Node materials are the foundation for building custom water shaders in TSL.
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
Grass
Three.js grass is built with InstancedMesh: one blade geometry (usually a couple of triangles or a simple bent-plane sha…
WebGPU vs WebGL
WebGPU is the successor to WebGL: a lower-level, more modern graphics API that exposes compute shaders and reduces CPU o…
Three.js vs Babylon.js
Three.js is a lower-level, unopinionated 3D rendering library with the largest ecosystem and community on the web, while…
All Three.js Guides
Back to the guides hub.



