Step‑by‑Step Ocean Simulation with three.js
This tutorial walks readers through building a realistic, animated ocean scene in three.js, covering project initialization, geometry creation, custom shaders, wave calculations, and dynamic boat positioning and rotation using JavaScript and WebGL techniques.
After a long hiatus, the author presents a detailed three.js tutorial that guides readers to render a dynamic ocean and a boat that reacts to wave motion.
1. Initialize the three.js project
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
let renderer, camera, scene, controls, clock, lineHelper;
// Renderer
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// Camera
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 10, 20);
// Resize handling
function resize() {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', resize, false);
// Scene and controls
scene = new THREE.Scene();
controls = new OrbitControls(camera, renderer.domElement);
clock = new THREE.Clock();
function render() {
requestAnimationFrame(render);
const elapsedTime = clock.getElapsedTime();
controls.update();
renderer.render(scene, camera);
}1.2 Add basic 3D objects
// Lights
const light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(0, 10, 20);
scene.add(light);
const light2 = new THREE.DirectionalLight(0xffffff, 0.1);
light2.position.set(-5, 5, -5);
scene.add(light2);
const ambient = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambient);
// Boat (a simple box)
box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshLambertMaterial());
scene.add(box);
// Direction line helper
const helperGeometry = new THREE.BufferGeometry();
helperGeometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array([0,0,0, 0,5,0]), 3));
const lineHelper = new THREE.LineSegments(helperGeometry, new THREE.MeshBasicMaterial({color: 0xff0000, depthTest:false}));
scene.add(lineHelper);2. Create the ocean surface
First a dense plane (100 × 100 with 500 × 500 vertices) is generated so that vertex positions can be altered later.
let material;
material = new THREE.ShaderMaterial({wireframe:true});
const geometry = new THREE.PlaneGeometry(100, 100, 500, 500);
geometry.rotateX(-Math.PI / 2);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);Next a custom vertex and fragment shader are added. The vertex shader computes a height value using several sine functions, while the fragment shader samples a water texture and adds color.
const SCALE = 5;
const vertexShader = `
#define SCALE ${SCALE}.0
varying vec2 vUv;
uniform float uTime;
float calculateSurface(float x, float z) {
float y = 0.0;
y += (sin(x * 1.0 / SCALE + uTime) + sin(x * 2.3 / SCALE + uTime * 1.5) + sin(x * 3.3 / SCALE + uTime * 0.4)) / 3.0;
y += (sin(z * 0.2 / SCALE + uTime * 1.8) + sin(z * 1.8 / SCALE + uTime * 1.8) + sin(z * 2.8 / SCALE + uTime * 0.8)) / 3.0;
return y;
}
void main(){
vUv = uv;
vec3 pos = position;
pos.y += calculateSurface(pos.x, pos.z);
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos,1.0);
}
`;
const fragmentShader = `
varying vec2 vUv;
uniform sampler2D uMap;
uniform float uTime;
uniform vec3 uColor;
void main(){
vec2 uv = vUv * 10.0 + vec2(uTime * -0.05);
uv.y += 0.01 * (sin(uv.x * 3.5 + uTime * 0.35) + sin(uv.x * 4.8 + uTime * 1.05) + sin(uv.x * 7.3 + uTime * 0.45)) / 3.0;
uv.x += 0.12 * (sin(uv.y * 4.0 + uTime * 0.5) + sin(uv.y * 6.8 + uTime * 0.75) + sin(uv.y * 11.3 + uTime * 0.2)) / 3.0;
vec4 tex1 = texture2D(uMap, uv);
vec4 tex2 = texture2D(uMap, uv + vec2(0.2));
vec3 blue = uColor;
gl_FragColor = vec4(blue + vec3(tex1.a * 0.9 - tex2.a * 0.02), 1.0);
}
`;
const texture = new THREE.TextureLoader().load('./textures/water.png');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
const uniforms = {uMap:{value:texture}, uTime:{value:0}, uColor:{value:new THREE.Color('#0051da')}, depthTest:true, depthWrite:true};
material = new THREE.ShaderMaterial({uniforms, vertexShader, fragmentShader, side:THREE.DoubleSide, wireframe:true});
const oceanGeometry = new THREE.PlaneGeometry(100,100,500,500);
oceanGeometry.rotateX(-Math.PI/2);
const oceanMesh = new THREE.Mesh(oceanGeometry, material);
scene.add(oceanMesh);In the render loop the uniform uTime is updated:
material.uniforms.uTime.value = clock.getElapsedTime();3. Update boat height according to the wave function
const position = box.position;
const {x, z} = position;
position.y = (sin(x / SCALE + elapsedTime) + sin(2.3*x / SCALE + 1.5*elapsedTime) + sin(3.3*x / SCALE + 0.4*elapsedTime)) / 3.0;
position.y += (sin(0.2*z / SCALE + 1.8*elapsedTime) + sin(1.8*z / SCALE + 1.8*elapsedTime) + sin(2.8*z / SCALE + 0.8*elapsedTime)) / 3.0;4. Compute boat orientation and acceleration
Derivatives of the wave function give the surface slope (kx, kz). The surface normal n = new THREE.Vector3(-kx, 1, -kz).normalize() and the rotation axis are derived via cross products, then the boat is rotated accordingly.
function dx(x,t){return 1/3*(Math.cos(x/SCALE + t)/SCALE + Math.cos(2.3*x/SCALE + 1.5*t)*2.3/SCALE + Math.cos(3.3*x/SCALE + 0.4*t)*3.3/SCALE);}
function dz(z,t){return 1/3*(Math.cos(0.2*z/SCALE + 1.8*t)*0.2/SCALE + Math.cos(1.8*z/SCALE + 1.8*t)*1.8/SCALE + Math.cos(2.8*z/SCALE + 0.8*t)*2.8/SCALE);}
const kx = dx(x, elapsedTime);
const kz = dz(z, elapsedTime);
const n = new THREE.Vector3(-kx, 1, -kz).normalize();
const axes = new THREE.Vector3().crossVectors(n, new THREE.Vector3(kx,1,kz)).normalize();
function getAngleBetweenVectors(v1,v2){let dot=v1.dot(v2); if(dot>0.99995) return 0; if(dot<-0.99995) return Math.PI; return Math.acos(dot);}
const angle = getAngleBetweenVectors(new THREE.Vector3(0,1,0), n);
box.rotation.set(0,0,0);
box.rotateOnAxis(axes, -angle);
// Acceleration
const speed = new THREE.Vector3();
const dir = new THREE.Vector3().crossVectors(n, axes).normalize().divideScalar(100);
const newSpeed = speed.add(dir);
const endPosition = box.position.clone().addScaledVector(newSpeed, 1);
const y = (sin(x / SCALE + elapsedTime) + sin(2.3*x / SCALE + 1.5*elapsedTime) + sin(3.3*x / SCALE + 0.4*elapsedTime)) / 3.0 +
(sin(0.2*z / SCALE + 1.8*elapsedTime) + sin(1.8*z / SCALE + 1.8*elapsedTime) + sin(2.8*z / SCALE + 0.8*elapsedTime)) / 3.0;
const truePosition = new THREE.Vector3(endPosition.x, y, endPosition.z);
box.position.copy(truePosition);The final result is an animated ocean with a boat that rises, tilts, and accelerates naturally as the waves move.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
