REALISTIC
WATER SHADER
Build a realistic animated ocean in the browser with Three.js — reflections, a dynamic sky, and a moving sun, using the built-in Water and Sky objects.
LIVE DEMO
The reflective ocean, dynamic sky, and floating mirror cube rendering behind this page are the live demo. Drag to orbit the camera and watch the sun highlights track across the waves.
Introduction
Realistic water used to be one of the hardest effects in real-time 3D. In Three.js it's almost turnkey: the library ships a ready-made Water object and a matching Sky object in its addons. Give the water a big plane and a tiling normal map, point a sun at it, and animate a time uniform — you get rippling, reflective ocean water in under 50 lines.
This free tutorial builds exactly that: an infinite reflective ocean, a physically-based sky with a sun you can position, and a chrome cube bobbing on the surface to show off the reflections. Copy the code below and it runs.
Frequently Asked Questions
How do you make realistic water in Three.js?
Three.js ships a ready-made Water object in its addons (three/addons/objects/Water.js). You give it a large plane geometry and a tiling water-normals texture, set a sun direction and water color, then advance its time uniform every frame. Combined with the Sky object you get a realistic reflective ocean with very little code.
What is the Three.js Water object?
Water is a built-in helper mesh in the Three.js addons that renders an animated, reflective water surface. It uses a normal map for ripples and reads a sun direction so highlights track your light. You animate it by incrementing water.material.uniforms['time'].value with the frame delta.
How do you add reflections to the water?
The Water object reflects the scene automatically via a reflection render target. Pair it with the Sky object and a PMREMGenerator environment map so floating objects pick up realistic sky reflections. Setting a floating cube's material roughness to 0 shows the mirror-like reflection clearly.
Is this Three.js water tutorial free?
Yes. This is a completely free tutorial. The full source code is shown on the page, you can copy it, and there is a live ocean demo running in the background. No sign-in or membership is required.
How To Add The Code
The Water and Sky objects live in the Three.js addons, so this uses ES modules with an import map. You need Three.js, its jsm/objects/Water.js and jsm/objects/Sky.js, and a water-normals texture.
- 1Create the HTML file and add an
importmapthat mapsthreeandthree/addons/to the Three.js build and jsm folders. - 2Import the pieces —
THREE,OrbitControls,Water, andSky. - 3Set up scene, camera & renderer with Reinhard tone mapping for a natural sky exposure.
- 4Create the Water from a huge plane and a tiling
waternormals.jpgtexture (set it to RepeatWrapping), rotate it flat, and add it to the scene. - 5Add the Sky and set its turbidity, rayleigh, and mie uniforms for a believable atmosphere.
- 6Position the sun — convert elevation/azimuth to a direction, feed it to both the sky and the water's
sunDirection, and bake an environment map withPMREMGenerator. - 7Add a mirror cube (roughness 0) so you can see the reflections, then animate it and advance the water's time uniform each frame.
- 8Serve the folder locally (textures need HTTP, not file://) and open it — you'll see a live reflective ocean.
Tip: grab waternormals.jpg from the Three.js examples (examples/textures/water/) and run a local server, e.g. python -m http.server.
The Full Code
Save as index.html next to your Three.js build/ and jsm/ folders and a waternormals.jpg, then serve the folder.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Three.js Water Shader</title>
<style> body { margin: 0; overflow: hidden; } </style>
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{ "imports": { "three": "./build/three.module.js", "three/addons/": "./jsm/" } }
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { Water } from 'three/addons/objects/Water.js';
import { Sky } from 'three/addons/objects/Sky.js';
const clock = new THREE.Clock();
// Renderer, scene, camera
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.toneMapping = THREE.ReinhardToneMapping;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 1, 20000);
camera.position.set(30, 30, 100);
const sun = new THREE.Vector3();
// Reflective water surface
const water = new Water(
new THREE.PlaneGeometry(10000, 10000),
{
textureWidth: 512,
textureHeight: 512,
waterNormals: new THREE.TextureLoader().load('waternormals.jpg', t => {
t.wrapS = t.wrapT = THREE.RepeatWrapping;
}),
sunDirection: new THREE.Vector3(),
sunColor: 0xffffff,
waterColor: 0x001e0f,
distortionScale: 3.7
}
);
water.rotation.x = -Math.PI / 2;
scene.add(water);
// Sky + sun
const sky = new Sky();
sky.scale.setScalar(10000);
scene.add(sky);
sky.material.uniforms['turbidity'].value = 10;
sky.material.uniforms['rayleigh'].value = 2;
sky.material.uniforms['mieCoefficient'].value = 0.005;
sky.material.uniforms['mieDirectionalG'].value = 0.8;
const pmrem = new THREE.PMREMGenerator(renderer);
const params = { elevation: 2, azimuth: 180 };
function updateSun() {
const phi = THREE.MathUtils.degToRad(90 - params.elevation);
const theta = THREE.MathUtils.degToRad(params.azimuth);
sun.setFromSphericalCoords(1, phi, theta);
sky.material.uniforms['sunPosition'].value.copy(sun);
water.material.uniforms['sunDirection'].value.copy(sun).normalize();
scene.environment = pmrem.fromScene(sky).texture;
}
updateSun();
// A mirror cube to show the reflections
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(30, 30, 30),
new THREE.MeshStandardMaterial({ roughness: 0 })
);
scene.add(mesh);
// Orbit camera
const controls = new OrbitControls(camera, renderer.domElement);
controls.maxPolarAngle = Math.PI * 0.495;
controls.target.set(0, 10, 0);
controls.minDistance = 40;
controls.maxDistance = 200;
controls.enableZoom = false; // let the mouse wheel scroll the page instead of zooming
controls.enablePan = false;
controls.update();
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
const t = performance.now() * 0.001;
mesh.position.y = Math.sin(t) * 20 + 5;
mesh.rotation.x += delta * 0.5;
mesh.rotation.z += delta * 0.51;
water.material.uniforms['time'].value += delta;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
What To Try Next
- Animate
params.elevationover time and callupdateSun()for a sunrise-to-sunset cycle. - Change
waterColorto a tropical turquoise or a stormy grey. - Raise
distortionScalefor choppier seas, lower it for a calm lake. - Float a GLTF boat or buoy on the surface and bob it with the same sine wave.
- Add fog matching the horizon color for a hazy, atmospheric ocean.
SEE 3D AND 2D BROWSER GAMES IN ACTION
Built with Three.js and JavaScript — free, no download, plays right in your browser.
Super Soldier Battle
Free 3D multiplayer FPS built with Three.js and WebGL. Destroy the enemy bots and turrets, climb the leaderboard.
Play now →
AI Tic-Tac-Toe
Play the classic game against an AI opponent right in your browser. Free, no download, with an online leaderboard.
Play now →
Zombie Attack
Survive waves of zombies in this free 3D browser shooter built with Three.js. No download, plays instantly.
Play now →
Space Wow
Blast through space in this free 3D Three.js arcade shooter. Free, no download, plays right in your browser.
Play now →