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 — recommended for bundled applications
- Plain JavaScript + CDN — no build tools required
TypeScript + Vite
Step 1: Create a Vite Project
npm create vite@latest my-ocean-project -- --template vanilla-ts
cd my-ocean-projectStep 2: Install Three.js
npm install three@^0.185.0
npm install --save-dev @types/three@^0.185.0Step 3: Add the Libraries
Unzip
threejs-sky-pro.zipandthreejs-water-pro.zip.Create
threejs-sky-proandthreejs-water-prosub-directories within yoursrcdirectory.Copy the contents of each package's
builddirectory into the matching directory you just created. For Sky Pro, copy everything, includingdata/— the cloud-noise volumes load from there at runtime.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.jsonStep 4: Update index.html
Replace the contents of index.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:
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
npm run devOpen 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.htmlStep 2: Add the Libraries
Unzip
threejs-sky-pro.zipandthreejs-water-pro.zip.Copy the contents of each package's
builddirectory into the matchinglibsub-folder. For Sky Pro, copy everything, includingdata/— 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.
<!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:
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:
# Using Python
python3 -m http.server 8080
# Using Node.js (npx)
npx serve .
# Using PHP
php -S localhost:8080Open 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)beforewater.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.
// 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:
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
- Reflections — capture position, performance, and update cadence
- Environment Maps
SkyProvider- Presets — serializable look snapshots
