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.

Three.js realistic water shader ocean effect

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.

  1. 1Create the HTML file and add an importmap that maps three and three/addons/ to the Three.js build and jsm folders.
  2. 2Import the piecesTHREE, OrbitControls, Water, and Sky.
  3. 3Set up scene, camera & renderer with Reinhard tone mapping for a natural sky exposure.
  4. 4Create the Water from a huge plane and a tiling waternormals.jpg texture (set it to RepeatWrapping), rotate it flat, and add it to the scene.
  5. 5Add the Sky and set its turbidity, rayleigh, and mie uniforms for a believable atmosphere.
  6. 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 with PMREMGenerator.
  7. 7Add a mirror cube (roughness 0) so you can see the reflections, then animate it and advance the water's time uniform each frame.
  8. 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.elevation over time and call updateSun() for a sunrise-to-sunset cycle.
  • Change waterColor to a tropical turquoise or a stormy grey.
  • Raise distortionScale for 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.