Building the Ocean with Three JS

By
Ken Kozma

When designing for a digital space, creating an intriguing and interesting experience is an important part that can often be overlooked. Inviting users to immerse themselves in your site requires more than just a flat page. Here at liquidfish, we love to push the boundaries of our creativity. A great way to accomplish this, is to create unique animation experiences. We like to do this with three.js.

Three.js is a 3D JavaScript library that utilizes WebGL and is an easy way to get into 3D web animations for beginners.

Today, we’re going to build a 3D ocean environment, including a sun and sky. Don’t worry if most of what we discuss doesn’t make much sense at first. It will eventually become easier with repetition. Three.js has concise documentation and there are plenty of other great resources here.

Every line of code that we write today can be found here:

Clean Ocean

I encourage you to code along with us as you explore this tutorial in codepen. Be sure to include the three.js library in your JavaScript and <div class="canvas"></div> in your HTML. With that out of the way, let's get started!

At the core of any three.js project, we need a camera, scene, and the renderer. An interesting way to visualize this is to think of it as if we are making a movie. We need something to film our movie (camera), we need somewhere to shoot it (scene), and a movie theatre to show it (renderer).

But first, let's import all our required JS modules we will need later.


import { OrbitControls } from 'https://threejs.org/examples/jsm/controls/OrbitControls.js';

import { Water } from 'https://threejs.org/examples/jsm/objects/Water.js';

import { Sky } from 'https://threejs.org/examples/jsm/objects/Sky.js';

  1. OrbitControls allow the camera to orbit around a target.
  2. Water and Sky are 3D shaders that three.js provides out of the box.

Next, we are going to define our main function where most of our logic will be placed. We'll call it the SceneManager().


function SceneManager(canvas) {
    // Magic goes here
}

I've broken down the components into functions that are easier to read and not a wild mess of code, which some three.js projects tend to become.

Like I said earlier, we need to set our scene, renderer, and camera.

A renderer is the main object in three.js, as it renders or draws the 3D space that we create based off of the scene and camera we give it.

To create a scene we simply call the scene object and return it inside of our buildScene function.


function buildScene() {
  const scene = new THREE.Scene();
  return scene;
}

Next, we want to create our camera. The PerspectiveCamera class accepts 4 parameters (field of view, aspect ratio, near plane and far plane).


function buildCamera() {
  const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 1, 20000);
  camera.position.set(30, 30, 100);
  return camera;
}

Next, we set our renderer. You may notice we pass in a canvas argument. This will be our html element where we want to place our 3D space inside.


function buildRenderer(canvas) {
  const renderer = new THREE.WebGLRenderer();
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  canvas.appendChild(renderer.domElement);
  return renderer;
}

Now that we have the core of any three.js project set up, we can now create and append objects to our scene. Let's start off by building the sky, using the sky shader we imported.

All we need to do is set the scalar and then append it to our scene with the add method.


function buildSky() {
  const sky = new Sky();
  sky.scale.setScalar(10000);
  scene.add(sky);
  return sky;
}

Next, we are going to build our sun.


function buildSun() {
  const pmremGenerator = new THREE.PMREMGenerator(renderer);
  const sun = new THREE.Vector3();

	// Defining the x, y and z value for our 3D Vector
  const theta = Math.PI * (0.49 - 0.5);
  const phi = 2 * Math.PI * (0.205 - 0.5);
  sun.x = Math.cos(phi);
  sun.y = Math.sin(phi) * Math.sin(theta);
  sun.z = Math.sin(phi) * Math.cos(theta);

  sky.material.uniforms['sunPosition'].value.copy(sun);
  scene.environment = pmremGenerator.fromScene(sky).texture;
  return sun;
}

  1. PMREMGenerator builds a Prefiltered, Mipmapped Radiance Environment Map (PMREM) from a cubeMap environment texture
  2. Vector3() defines a 3D Vector and is an ordered triplet of numbers (labeled x, y, and z)

​Next, we are going to build our ocean or water plane.


function buildWater() {
  const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
  const water = new Water(
    waterGeometry,
    {
      textureWidth: 512,
      textureHeight: 512,
      waterNormals: new THREE.TextureLoader().load('', function ( texture ) {
        texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
      }),
      alpha: 1.0,
      sunDirection: new THREE.Vector3(),
      sunColor: 0xffffff,
      waterColor: 0x001e0f,
      distortionScale: 3.7,
      fog: scene.fog !== undefined
    }
  );
  water.rotation.x =- Math.PI / 2;
  scene.add(water);
  
  const waterUniforms = water.material.uniforms;
  return water;
}

And just for fun, let’s create a simple sphere and set our orbit controls.


function buildSphere() {
  const geometry = new THREE.SphereGeometry(20, 20, 20);
  const material = new THREE.MeshStandardMaterial({color: 0xfcc742});

  const sphere = new THREE.Mesh(geometry, material);
  scene.add(sphere);
  return sphere;
}

function setOrbitControls() {
  const controls = new OrbitControls(camera, renderer.domElement);
  controls.maxPolarAngle = Math.PI * 0.495;
  controls.target.set(0, 10, 0);
  controls.minDistance = 40.0;
  controls.maxDistance = 200.0;
  controls.update();
  return controls;
}

We'll define our function and call in our animate loop, which will redraw the scene every time the screen is refreshed (typically around 60 times per second).


this.update = function() {
  // Animates our water
  water.material.uniforms[ 'time' ].value += 1.0 / 60.0;

	// Reposition our sphere to appear to float up and down
  const time = performance.now() * 0.001;
  sphere.position.y = Math.sin( time ) * 2;
  sphere.rotation.x = time * 0.3;
  sphere.rotation.z = time * 0.3;
	
	// Finally, render our scene
  renderer.render(scene, camera);
}

To allow our scene to adjust to the browser window being resized, we set the following event listener and its callback to adjust accordingly.


function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onWindowResize);

Now at the very top of our SceneManager function, we can call all the functions we just made, select our canvas element we've defined in our HTML, and call our sceneManager function while passing in our element as an argument.


const scene = buildScene();
const renderer = buildRenderer(canvas);
const camera = buildCamera();
const sphere = buildSphere();
const sky = buildSky();
const sun = buildSun();
const water = buildWater();
const orbitCon = setOrbitControls();

const canvas = document.getElementById("canvas");
const sceneManager = new SceneManager(canvas);

Last but not least, we’ll create an animate function and immediately call it to render and animate our scene. We’ll also call our update method from the SceneManager.


function animate() {
  requestAnimationFrame(animate);
  sceneManager.update();
}
animate();

By now, you should see a nice, calm ocean with a glowing sunset… along with a random sphere floating up and down. Oh, the joys of three.js!

There you have it, we just built a 3D ocean in three.js. Although I didn’t explain all of the concepts in depth within this blog, I encourage you to keep learning about three.js through the useful links provided above and through the three.js documentation.

Happy coding!