Skip to content

Water Pro Integration

Combine Three.js Sky Pro with Three.js Water Pro to render an ocean beneath an animated sky. A sky provider supplies Water Pro with reflections, fog color, and sun lighting.

This guide requires a licensed copy of each library. Choose a setup:


TypeScript + Vite

Step 1: Create a Vite Project

bash
npm create vite@latest my-ocean-project -- --template vanilla-ts
cd my-ocean-project

Step 2: Install Three.js

bash
npm install three@^0.185.0
npm install --save-dev @types/three@^0.185.0

Step 3: Add the Libraries

  1. Unzip threejs-sky-pro.zip and threejs-water-pro.zip.

  2. Create threejs-sky-pro and threejs-water-pro sub-directories within your src directory.

  3. Copy the contents of each package's build directory into the matching directory you just created. For Sky Pro, copy everything, including data/ — the cloud-noise volumes load from there at runtime.

  4. Confirm the following file structure:

my-ocean-project/
├── src/
│   ├── threejs-sky-pro/
│   │   ├── index.js      ← Sky Pro library bundle
│   │   ├── index.js.map  ← Source map
│   │   ├── index.d.ts    ← TypeScript declarations
│   │   └── data/         ← Cloud-noise volumes
│   ├── threejs-water-pro/
│   │   ├── index.js      ← Water Pro library bundle
│   │   ├── index.js.map  ← Source map
│   │   └── index.d.ts    ← TypeScript declarations
│   └── main.ts
├── index.html
└── package.json

Step 4: Update index.html

Replace the contents of index.html:

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js Sky Pro + Water Pro</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        overflow: hidden;
        background: #000;
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Step 5: Add the Application Code

Replace src/main.ts with:

typescript
import * as THREE from "three/webgpu";
import { pass, vec4 } from "three/tsl";
import { bloom } from "three/addons/tsl/display/BloomNode.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { WaterSystem, getPresetParams } from "./threejs-water-pro";
import { SkySystem, PRESETS } from "./threejs-sky-pro";

function setupPostProcessing(
  renderer: THREE.WebGPURenderer,
  water: WaterSystem,
  sky: SkySystem,
): THREE.RenderPipeline {
  const scenePass = pass(water.scene, water.camera);
  let output = scenePass.getTextureNode("output");

  // Add water effects (atmospheric fog, underwater haze, sun shafts)
  output = water.postProcessing.buildNode(scenePass, output);

  // Add distance fog and god rays. Clouds already render as scene backdrops.
  // The scene pass supplies depth to both post-processing stages.
  output = sky.applyTo(output, scenePass);

  // Apply the sky's exposure before bloom and the renderer's tone map.
  output = vec4(output.rgb.mul(sky.atmosphere.exposure), output.a);

  // Add bloom
  output = output.add(bloom(output, 0.5, 0.4, 0.85));

  return new THREE.RenderPipeline(renderer, output);
}

async function main() {
  // Create renderer
  const renderer = new THREE.WebGPURenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1));
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  document.body.appendChild(renderer.domElement);
  await renderer.init();

  // Create scene and camera
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    0.1,
    50000,
  );
  camera.position.set(50, 25, 50);

  // Add orbit controls
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;

  // Create water system
  const water = await WaterSystem.create(renderer, scene, camera, "high");
  water.loadPreset(getPresetParams("blackFlag"));

  // Create the sky. SkySystem.create adds its backdrop meshes to the scene
  // for you; applyPreset sets the look (clouds, atmosphere, sun).
  const sky = await SkySystem.create({ renderer, camera, scene });
  await sky.applyPreset(PRESETS.partlyCloudy);
  sky.sun.setFromAngles(35, 120);

  // Wire the sky into the water. The provider drives the water's sun
  // lighting, reflections, and fog color. `envMap: true` bakes an
  // environment map so reflections include the clouds.
  water.setSky(sky.createSkyProvider({ envMap: true }));

  for (let i = 0; i < 10; i++) {
    const geometry = new THREE.BoxGeometry(4, 4, 4);
    const material = new THREE.MeshStandardMaterial({
      color: new THREE.Color(Math.random(), Math.random(), Math.random()),
    });
    const box = new THREE.Mesh(geometry, material);

    // Position boxes in a scattered pattern
    const angle = (i / 10) * Math.PI * 2;
    const distance = 10 + Math.random() * 25;
    box.position.set(Math.cos(angle) * distance, 0, Math.sin(angle) * distance);

    // Add box to the scene
    scene.add(box);

    // Add to buoyancy system
    water.buoyancy.addObject(box, {
      heightSmoothing: 0.15,
      rotationSmoothing: 0.1,
    });
  }

  // Set up post-processing
  const postProcessing = setupPostProcessing(renderer, water, sky);

  // Handle window resize
  window.addEventListener("resize", () => {
    const w = window.innerWidth;
    const h = window.innerHeight;
    renderer.setSize(w, h);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    water.resize();
    sky.resize(w, h);
  });

  // Compile shaders before starting animation
  await renderer.compileAsync(scene, camera);

  // Animation loop
  let lastTime = performance.now();

  async function animate() {
    requestAnimationFrame(animate);

    const now = performance.now();
    const deltaTime = (now - lastTime) / 1000;
    lastTime = now;

    controls.update();
    sky.update(deltaTime);
    await water.update(deltaTime);
    postProcessing.render();
  }

  animate();
}

main();

Night sky

The star panorama is not bundled. Without one, stars are disabled and the moon uses a flat tinted disc. This example keeps the sun up, so no starmap is needed. To add stars to a day/night cycle, load an equirectangular starmap and pass it to SkySystem.create as nightSky: { texture }. See Day/Night Cycle for setup and properly licensed starmaps.

Step 6: Run the Project

bash
npm run dev

Open the URL shown in the terminal, usually http://localhost:5173.


Plain JavaScript + CDN

This setup loads Three.js from a CDN through an import map.

Step 1: Create Project Files

Create a folder with the following structure:

my-ocean-project/
├── lib/
│   ├── threejs-sky-pro/
│   └── threejs-water-pro/
├── src/
│   └── main.js
└── index.html

Step 2: Add the Libraries

  1. Unzip threejs-sky-pro.zip and threejs-water-pro.zip.

  2. Copy the contents of each package's build directory into the matching lib sub-folder. For Sky Pro, copy everything, including data/ — the cloud-noise volumes load from there at runtime.

Step 3: Create index.html

Use an import map to load Three.js and its addons from the same CDN version.

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js Sky Pro + Water Pro</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        overflow: hidden;
        background: #000;
      }
      canvas {
        display: block;
      }
    </style>

    <!-- Import map for Three.js CDN -->
    <script type="importmap">
      {
        "imports": {
          "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.webgpu.min.js",
          "three/webgpu": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.webgpu.min.js",
          "three/tsl": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.tsl.min.js",
          "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
        }
      }
    </script>
  </head>
  <body>
    <script type="module" src="./src/main.js"></script>
  </body>
</html>

Step 4: Create main.js

Use the TypeScript example from Step 5 with JavaScript imports and no type annotations:

javascript
import * as THREE from "three/webgpu";
import { pass, vec4 } from "three/tsl";
import { bloom } from "three/addons/tsl/display/BloomNode.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { WaterSystem, getPresetParams } from "../lib/threejs-water-pro/index.js";
import { SkySystem, PRESETS } from "../lib/threejs-sky-pro/index.js";

function setupPostProcessing(renderer, water, sky) {
  const scenePass = pass(water.scene, water.camera);
  let output = scenePass.getTextureNode("output");

  // Add water effects (atmospheric fog, underwater haze, sun shafts)
  output = water.postProcessing.buildNode(scenePass, output);

  // Add distance fog and god rays. Clouds already render as scene backdrops.
  // The scene pass supplies depth to both post-processing stages.
  output = sky.applyTo(output, scenePass);

  // Apply the sky's exposure before bloom and the renderer's tone map.
  output = vec4(output.rgb.mul(sky.atmosphere.exposure), output.a);

  // Add bloom
  output = output.add(bloom(output, 0.5, 0.4, 0.85));

  return new THREE.RenderPipeline(renderer, output);
}

async function main() {
  // Create renderer
  const renderer = new THREE.WebGPURenderer();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1));
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  document.body.appendChild(renderer.domElement);
  await renderer.init();

  // Create scene and camera
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(
    60,
    window.innerWidth / window.innerHeight,
    0.1,
    50000,
  );
  camera.position.set(50, 25, 50);

  // Add orbit controls
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.dampingFactor = 0.05;

  // Create water system
  const water = await WaterSystem.create(renderer, scene, camera, "high");
  water.loadPreset(getPresetParams("blackFlag"));

  // Create the sky. SkySystem.create adds its backdrop meshes to the scene
  // for you; applyPreset sets the look (clouds, atmosphere, sun).
  const sky = await SkySystem.create({ renderer, camera, scene });
  await sky.applyPreset(PRESETS.partlyCloudy);
  sky.sun.setFromAngles(35, 120);

  // Wire the sky into the water. The provider drives the water's sun
  // lighting, reflections, and fog color. `envMap: true` bakes an
  // environment map so reflections include the clouds.
  water.setSky(sky.createSkyProvider({ envMap: true }));

  for (let i = 0; i < 10; i++) {
    const geometry = new THREE.BoxGeometry(4, 4, 4);
    const material = new THREE.MeshStandardMaterial({
      color: new THREE.Color(Math.random(), Math.random(), Math.random()),
    });
    const box = new THREE.Mesh(geometry, material);

    // Position boxes in a scattered pattern
    const angle = (i / 10) * Math.PI * 2;
    const distance = 10 + Math.random() * 25;
    box.position.set(Math.cos(angle) * distance, 0, Math.sin(angle) * distance);

    // Add box to the scene
    scene.add(box);

    // Add to buoyancy system
    water.buoyancy.addObject(box, {
      heightSmoothing: 0.15,
      rotationSmoothing: 0.1,
    });
  }

  // Set up post-processing
  const postProcessing = setupPostProcessing(renderer, water, sky);

  // Handle window resize
  window.addEventListener("resize", () => {
    const w = window.innerWidth;
    const h = window.innerHeight;
    renderer.setSize(w, h);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
    water.resize();
    sky.resize(w, h);
  });

  // Compile shaders before starting animation
  await renderer.compileAsync(scene, camera);

  // Animation loop
  let lastTime = performance.now();

  async function animate() {
    requestAnimationFrame(animate);

    const now = performance.now();
    const deltaTime = (now - lastTime) / 1000;
    lastTime = now;

    controls.update();
    sky.update(deltaTime);
    await water.update(deltaTime);
    postProcessing.render();
  }

  animate();
}

main();

Step 5: Serve the Files

Serve the files over HTTP because browsers block ES modules loaded through file://. Use one of these commands:

bash
# Using Python
python3 -m http.server 8080

# Using Node.js (npx)
npx serve .

# Using PHP
php -S localhost:8080

Open http://localhost:8080 in your browser.


How the Integration Works

  • Sky provider: water.setSky(sky.createSkyProvider({ envMap: true })) supplies cloud reflections, fog color, and live sun values to Water Pro.
  • Update order: Call sky.update(deltaTime) before water.update(deltaTime) each frame, so the water samples the current frame's sky.
  • Post-processing order: Apply Water Pro effects first, then sky.applyTo, exposure, bloom, and tone mapping. Both systems read scene depth.

Cloud Reflections

createSkyProvider({ envMap: true }) includes clouds through a periodically updated environment map.

createSkyProvider() omits cloud reflections but retains the atmosphere, sun and moon discs, and any configured star panorama.

typescript
// Cloud reflections
water.setSky(sky.createSkyProvider({ envMap: true }));

// Cloud-free sky; no cloud raymarch.
water.setSky(sky.createSkyProvider());

Create the provider once and reuse it. Creating another cloud-enabled provider disposes the previous system-owned environment map.

Configure the Environment Map

Pass SkyEnvironmentOptions through the envMap field:

typescript
water.setSky(
  sky.createSkyProvider({
    envMap: {
      width: 1024,
      cloudMarchSteps: 32,
    },
  }),
);

Higher resolutions produce sharper reflections and increase GPU cost.

Result

The result is an ocean scene with floating boxes, cloud reflections, and shared sun lighting.

Next Steps

Commercial License - All Rights Reserved.