The Making of “The Aviator”: Animating a Basic 3D Scene with Three.js

A tutorial that explores the basics of creating animated 3D scenes using Three.js.

Today, we are going to create a simple 3D flying plane using Three.js, a 3D library that makes WebGL simpler. WebGL is a pretty unknown world for many developers because of the complexity and syntax of GLSL. But With Three.js, 3D in the browser becomes very easy to implement.

In this tutorial we’ll create a simple 3D scene with a few interactions in two major parts. In the first part we will explain the basics of Three.js and how to set up a very simple scene. The second part will go into some details on how to refine the shapes, how to add some atmosphere and better movements to the different elements of the scene.

Beyond the scope of this tutorial is the entire game, but you can download it and check out the code; it contains many interesting additional parts like the collisions, grabbing coins and increasing a score. Also, there’s a second version built by Michel Helms with more features, like sound, coins and weapons. 

In this tutorial we will focus on some basic concepts that will get you started in the world of WebGL with Three.js!

Animated3DScene_Part1_Part2

Let’s get started right away!

The HTML & CSS

This tutorial uses mainly the Three.js library, which makes WebGL easy to use. Check out the website and GitHub repo to get some more info on it.

The first thing to do is to import the library in your HTML header:

<script type="text/javascript" src="js/three.js"></script>

Then you need to add a container element in the HTML to hold the rendered scene:

<div id="world"></div>

You can simply style it like the following to make it fill the entire viewport:

#world {
	position: absolute;
	width: 100%;
	height: 100%;
	overflow: hidden;
	background: linear-gradient(#e4e0ba, #f7d9aa);
}

As you can see, the background has a subtle gradient that will resemble the sky.

And that’s it for the markup and style!

The JavaScript

Three.js is very easy to use if you have some basic knowledge of JavaScript. Let’s have a look at the different parts of the code we are going to implement.

The Color Palette

Animated3DScene_palette

Before starting to code the scene, I always find it very useful to define a color palette that will be used consistently throughout the project. For this project we choose the following colors:

var Colors = {
	red:0xf25346,
	white:0xd8d0d1,
	brown:0x59332e,
	pink:0xF5986E,
	brownDark:0x23190f,
	blue:0x68c3c0,
};

The Structure of the Code

Although the JavaScript code is pretty verbose, its structure is quite simple. All the main functions we need to create are put into the init function:

window.addEventListener('load', init, false);

function init() {
	// set up the scene, the camera and the renderer
	createScene();

	// add the lights
	createLights();

	// add the objects
	createPlane();
	createSea();
	createSky();

	// start a loop that will update the objects' positions 
	// and render the scene on each frame
	loop();
}

Setting up the Scene

To create a Three.js project, we’ll need at least the following:

  1. A scene: consider this as the stage where every object needs to be added in order to be rendered
  2. A camera: in this case we will use a perspective camera, but it could also be an orthographic camera.
  3. A renderer that will display all the scene using WebGL.
  4. One or more objects to render, in our case, we will create a plane, a sea and a sky (a few clouds)
  5. One or more lights: there is also different types of lights available. In this project we will mainly use a hemisphere light for the atmosphere and a directional light for the shadows.

Animated3DScene_three-components

The scene, the camera, and the renderer are created in the createScene function:

var scene,
		camera, fieldOfView, aspectRatio, nearPlane, farPlane, HEIGHT, WIDTH,
		renderer, container;

function createScene() {
	// Get the width and the height of the screen,
	// use them to set up the aspect ratio of the camera 
	// and the size of the renderer.
	HEIGHT = window.innerHeight;
	WIDTH = window.innerWidth;

	// Create the scene
	scene = new THREE.Scene();

	// Add a fog effect to the scene; same color as the
	// background color used in the style sheet
	scene.fog = new THREE.Fog(0xf7d9aa, 100, 950);
	
	// Create the camera
	aspectRatio = WIDTH / HEIGHT;
	fieldOfView = 60;
	nearPlane = 1;
	farPlane = 10000;
	camera = new THREE.PerspectiveCamera(
		fieldOfView,
		aspectRatio,
		nearPlane,
		farPlane
		);
	
	// Set the position of the camera
	camera.position.x = 0;
	camera.position.z = 200;
	camera.position.y = 100;
	
	// Create the renderer
	renderer = new THREE.WebGLRenderer({ 
		// Allow transparency to show the gradient background
		// we defined in the CSS
		alpha: true, 

		// Activate the anti-aliasing; this is less performant,
		// but, as our project is low-poly based, it should be fine :)
		antialias: true 
	});

	// Define the size of the renderer; in this case,
	// it will fill the entire screen
	renderer.setSize(WIDTH, HEIGHT);
	
	// Enable shadow rendering
	renderer.shadowMap.enabled = true;
	
	// Add the DOM element of the renderer to the 
	// container we created in the HTML
	container = document.getElementById('world');
	container.appendChild(renderer.domElement);
	
	// Listen to the screen: if the user resizes it
	// we have to update the camera and the renderer size
	window.addEventListener('resize', handleWindowResize, false);
}

As the screen size can change, we need to update the renderer size and the camera aspect ratio:

function handleWindowResize() {
	// update height and width of the renderer and the camera
	HEIGHT = window.innerHeight;
	WIDTH = window.innerWidth;
	renderer.setSize(WIDTH, HEIGHT);
	camera.aspect = WIDTH / HEIGHT;
	camera.updateProjectionMatrix();
}

The Lights

Lightning is certainly one of the trickiest parts when it comes to setting up a scene. The lights will set the mood of the whole scene and must be determined carefully. At this step of the project, we will just try to make the lightning good enough to make the objects visible.

var hemisphereLight, shadowLight;

function createLights() {
	// A hemisphere light is a gradient colored light; 
	// the first parameter is the sky color, the second parameter is the ground color, 
	// the third parameter is the intensity of the light
	hemisphereLight = new THREE.HemisphereLight(0xaaaaaa,0x000000, .9)
	
	// A directional light shines from a specific direction. 
	// It acts like the sun, that means that all the rays produced are parallel. 
	shadowLight = new THREE.DirectionalLight(0xffffff, .9);

	// Set the direction of the light  
	shadowLight.position.set(150, 350, 350);
	
	// Allow shadow casting 
	shadowLight.castShadow = true;

	// define the visible area of the projected shadow
	shadowLight.shadow.camera.left = -400;
	shadowLight.shadow.camera.right = 400;
	shadowLight.shadow.camera.top = 400;
	shadowLight.shadow.camera.bottom = -400;
	shadowLight.shadow.camera.near = 1;
	shadowLight.shadow.camera.far = 1000;

	// define the resolution of the shadow; the higher the better, 
	// but also the more expensive and less performant
	shadowLight.shadow.mapSize.width = 2048;
	shadowLight.shadow.mapSize.height = 2048;
	
	// to activate the lights, just add them to the scene
	scene.add(hemisphereLight);  
	scene.add(shadowLight);
}

As you can see here, a lot of parameters are used to create the lights. Do not hesitate to experiment with the colors, intensities and number of lights; you’ll discover interesting moods and ambiances for your scene and get a feel for how to tune them for your needs.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

Creating an Object with Three.js

If you are comfortable with 3D modeling software, you can create your objects there and simply import them into your Three.js project. This solution won’t be covered in this tutorial but we will instead create our objects using the primitives available in Three.js in order to get a better understanding of how they work.

Three.js has already a great number of ready-to-use primitives like a cube, a sphere, a torus, a cylinder and a plane.

For our project, all the objects we will create are simply a combination of these primitives. That’s perfectly fitting for a low-poly style, and it will spare us from having to model the objects in a 3D software.

A Simple Cylinder for the Sea

Let’s start with creating the sea as it is the easiest object we have to deal with. To keep things simple for now, we will illustrate the sea as a simple blue cylinder placed at the bottom of the screen. Later on we will dive into some details on how to refine this shape.

Next, let’s make the sea look a bit more attractive and the waves more realistic:

// First let's define a Sea object :
Sea = function(){
	
	// create the geometry (shape) of the cylinder;
	// the parameters are: 
	// radius top, radius bottom, height, number of segments on the radius, number of segments vertically
	var geom = new THREE.CylinderGeometry(600,600,800,40,10);
	
	// rotate the geometry on the x axis
	geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
	
	// create the material 
	var mat = new THREE.MeshPhongMaterial({
		color:Colors.blue,
		transparent:true,
		opacity:.6,
		shading:THREE.FlatShading,
	});

	// To create an object in Three.js, we have to create a mesh 
	// which is a combination of a geometry and some material
	this.mesh = new THREE.Mesh(geom, mat);

	// Allow the sea to receive shadows
	this.mesh.receiveShadow = true; 
}

// Instantiate the sea and add it to the scene:

var sea;

function createSea(){
	sea = new Sea();

	// push it a little bit at the bottom of the scene
	sea.mesh.position.y = -600;

	// add the mesh of the sea to the scene
	scene.add(sea.mesh);
}

Let’s summarize what we need in order to create an object. We need to

  1. create a geometry
  2. create a material
  3. pass them into a mesh
  4. add the mesh to our scene

With these basic steps, we can create many different kinds of primitive objects. Now, if we combine them, we can create much more complex shapes.

In the following steps we will learn how to do that precisely.

Combining Simple Cubes to Create a Complex Shape

The clouds are a little bit more complex, as they are a number of cubes assembled randomly to form one shape.

Animated3DScene_clouds

Cloud = function(){
	// Create an empty container that will hold the different parts of the cloud
	this.mesh = new THREE.Object3D();
	
	// create a cube geometry;
	// this shape will be duplicated to create the cloud
	var geom = new THREE.BoxGeometry(20,20,20);
	
	// create a material; a simple white material will do the trick
	var mat = new THREE.MeshPhongMaterial({
		color:Colors.white,  
	});
	
	// duplicate the geometry a random number of times
	var nBlocs = 3+Math.floor(Math.random()*3);
	for (var i=0; i<nBlocs; i++ ){
		
		// create the mesh by cloning the geometry
		var m = new THREE.Mesh(geom, mat); 
		
		// set the position and the rotation of each cube randomly
		m.position.x = i*15;
		m.position.y = Math.random()*10;
		m.position.z = Math.random()*10;
		m.rotation.z = Math.random()*Math.PI*2;
		m.rotation.y = Math.random()*Math.PI*2;
		
		// set the size of the cube randomly
		var s = .1 + Math.random()*.9;
		m.scale.set(s,s,s);
		
		// allow each cube to cast and to receive shadows
		m.castShadow = true;
		m.receiveShadow = true;
		
		// add the cube to the container we first created
		this.mesh.add(m);
	} 
}

Now that we have a cloud we will use it to create an entire sky by duplicating it, and placing it at random positions around the z-axis:

// Define a Sky Object
Sky = function(){
	// Create an empty container
	this.mesh = new THREE.Object3D();
	
	// choose a number of clouds to be scattered in the sky
	this.nClouds = 20;
	
	// To distribute the clouds consistently,
	// we need to place them according to a uniform angle
	var stepAngle = Math.PI*2 / this.nClouds;
	
	// create the clouds
	for(var i=0; i<this.nClouds; i++){
		var c = new Cloud();
	 
		// set the rotation and the position of each cloud;
		// for that we use a bit of trigonometry
		var a = stepAngle*i; // this is the final angle of the cloud
		var h = 750 + Math.random()*200; // this is the distance between the center of the axis and the cloud itself

		// Trigonometry!!! I hope you remember what you've learned in Math :)
		// in case you don't: 
		// we are simply converting polar coordinates (angle, distance) into Cartesian coordinates (x, y)
		c.mesh.position.y = Math.sin(a)*h;
		c.mesh.position.x = Math.cos(a)*h;

		// rotate the cloud according to its position
		c.mesh.rotation.z = a + Math.PI/2;

		// for a better result, we position the clouds 
		// at random depths inside of the scene
		c.mesh.position.z = -400-Math.random()*400;
		
		// we also set a random scale for each cloud
		var s = 1+Math.random()*2;
		c.mesh.scale.set(s,s,s);

		// do not forget to add the mesh of each cloud in the scene
		this.mesh.add(c.mesh);  
	}  
}

// Now we instantiate the sky and push its center a bit
// towards the bottom of the screen

var sky;

function createSky(){
	sky = new Sky();
	sky.mesh.position.y = -600;
	scene.add(sky.mesh);
}

Even More Complex: Creating The Airplane

The bad news is that the code for creating the airplane is a bit more lengthy and complex. But the good news is that we already learned everything we need to know in order to do it! It’s all about combining and encapsulating shapes.

Animated3DScene_plane-of-cubes

var AirPlane = function() {
	
	this.mesh = new THREE.Object3D();
	
	// Create the cabin
	var geomCockpit = new THREE.BoxGeometry(60,50,50,1,1,1);
	var matCockpit = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});
	var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
	cockpit.castShadow = true;
	cockpit.receiveShadow = true;
	this.mesh.add(cockpit);
	
	// Create the engine
	var geomEngine = new THREE.BoxGeometry(20,50,50,1,1,1);
	var matEngine = new THREE.MeshPhongMaterial({color:Colors.white, shading:THREE.FlatShading});
	var engine = new THREE.Mesh(geomEngine, matEngine);
	engine.position.x = 40;
	engine.castShadow = true;
	engine.receiveShadow = true;
	this.mesh.add(engine);
	
	// Create the tail
	var geomTailPlane = new THREE.BoxGeometry(15,20,5,1,1,1);
	var matTailPlane = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});
	var tailPlane = new THREE.Mesh(geomTailPlane, matTailPlane);
	tailPlane.position.set(-35,25,0);
	tailPlane.castShadow = true;
	tailPlane.receiveShadow = true;
	this.mesh.add(tailPlane);
	
	// Create the wing
	var geomSideWing = new THREE.BoxGeometry(40,8,150,1,1,1);
	var matSideWing = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});
	var sideWing = new THREE.Mesh(geomSideWing, matSideWing);
	sideWing.castShadow = true;
	sideWing.receiveShadow = true;
	this.mesh.add(sideWing);
	
	// propeller
	var geomPropeller = new THREE.BoxGeometry(20,10,10,1,1,1);
	var matPropeller = new THREE.MeshPhongMaterial({color:Colors.brown, shading:THREE.FlatShading});
	this.propeller = new THREE.Mesh(geomPropeller, matPropeller);
	this.propeller.castShadow = true;
	this.propeller.receiveShadow = true;
	
	// blades
	var geomBlade = new THREE.BoxGeometry(1,100,20,1,1,1);
	var matBlade = new THREE.MeshPhongMaterial({color:Colors.brownDark, shading:THREE.FlatShading});
	
	var blade = new THREE.Mesh(geomBlade, matBlade);
	blade.position.set(8,0,0);
	blade.castShadow = true;
	blade.receiveShadow = true;
	this.propeller.add(blade);
	this.propeller.position.set(50,0,0);
	this.mesh.add(this.propeller);
};
This airplane looks way to simple, right?
Don’t worry, later on we will see how to refine the shapes of the airplane to make it look much better!

Now, we can instantiate the airplane and add it to our scene:

var airplane;

function createPlane(){ 
	airplane = new AirPlane();
	airplane.mesh.scale.set(.25,.25,.25);
	airplane.mesh.position.y = 100;
	scene.add(airplane.mesh);
}

Rendering

We have created a couple of objects and added them to our scene. But if you try to run the game, you won’t be able to see anything! That’s because we still have to render the scene. We can simply do that by adding this line of code:

renderer.render(scene, camera);

Animation

Let’s bring some life to our scene by making the airplane’s propeller spin and by rotating the sea and the clouds.

For this we will need an infinite loop:

function loop(){
	// Rotate the propeller, the sea and the sky
	airplane.propeller.rotation.x += 0.3;
	sea.mesh.rotation.z += .005;
	sky.mesh.rotation.z += .01;

	// render the scene
	renderer.render(scene, camera);

	// call the loop function again
	requestAnimationFrame(loop);
}

As you can see, we have moved the call to the render method to the loop function. That’s because each change we make to an object needs to be rendered again.

Follow the Mouse: Adding Interaction

At this moment, we can see our airplane placed in the center of the scene. What we want to achieve next, is to make it follow the mouse movements.

Once the document is loaded, we need to add a listener to the document, to check if the mouse is moving.
For that, we’ll modify the init function as follows:

function init(event){
	createScene();
	createLights();
	createPlane();
	createSea();
	createSky();

	//add the listener
	document.addEventListener('mousemove', handleMouseMove, false);
	
	loop();
}

Additionally, we’ll create a new function to handle the mousemove event:

var mousePos={x:0, y:0};

// now handle the mousemove event

function handleMouseMove(event) {

	// here we are converting the mouse position value received 
	// to a normalized value varying between -1 and 1;
	// this is the formula for the horizontal axis:
	
	var tx = -1 + (event.clientX / WIDTH)*2;

	// for the vertical axis, we need to inverse the formula 
	// because the 2D y-axis goes the opposite direction of the 3D y-axis
	
	var ty = 1 - (event.clientY / HEIGHT)*2;
	mousePos = {x:tx, y:ty};

}

Now that we have a normalized x and y position of the mouse, we can move the airplane properly.

We need to modify the loop and add a new function to update the airplane:

function loop(){
	sea.mesh.rotation.z += .005;
	sky.mesh.rotation.z += .01;

	// update the plane on each frame
	updatePlane();
	
	renderer.render(scene, camera);
	requestAnimationFrame(loop);
}

function updatePlane(){

	// let's move the airplane between -100 and 100 on the horizontal axis, 
	// and between 25 and 175 on the vertical axis,
	// depending on the mouse position which ranges between -1 and 1 on both axes;
	// to achieve that we use a normalize function (see below)
	
	var targetX = normalize(mousePos.x, -1, 1, -100, 100);
	var targetY = normalize(mousePos.y, -1, 1, 25, 175);

	// update the airplane's position
	airplane.mesh.position.y = targetY;
	airplane.mesh.position.x = targetX;
	airplane.propeller.rotation.x += 0.3;
}

function normalize(v,vmin,vmax,tmin, tmax){

	var nv = Math.max(Math.min(v,vmax), vmin);
	var dv = vmax-vmin;
	var pc = (nv-vmin)/dv;
	var dt = tmax-tmin;
	var tv = tmin + (pc*dt);
	return tv;

}

Congratulations, with this, you’ve made the airplane follow your mouse movements! Have a look at what we have achieved so far: Demo of part 1

(Almost) Done!

As you can see, Three.js helps tremendously with creating WebGL content. You don’t need to know a lot to set up a scene and render a few custom objects. Until now you’ve learned a couple of basic concepts and with this you can already start getting the hang of it by tweaking a few parameters like the light intensity, the fog color and the size of the objects. Maybe you are even comfortable with creating some new objects by now?

If you would like to learn some more in-depth techniques, continue reading as you are about to learn how to refine the 3D scene, make the airplane move much more smoothly, and simulate a low-poly wave effect on the sea.

A Cooler Airplane!

Well, the airplane we have created previously is very basic. We know now how to create objects and combine them but we still need to learn how to modify a primitive to make it fit to our needs better.

A cube, for example, can be modified by moving its vertices. In our case we want to make it look more like a cockpit.

Let’s take a look at the cockpit part of the airplane and see how we can make it narrower in the back:

Animated3DScene_geometry-manipulation

// Cockpit

var geomCockpit = new THREE.BoxGeometry(80,50,50,1,1,1);
var matCockpit = new THREE.MeshPhongMaterial({color:Colors.red, shading:THREE.FlatShading});

// we can access a specific vertex of a shape through 
// the vertices array, and then move its x, y and z property:
geomCockpit.vertices[4].y-=10;
geomCockpit.vertices[4].z+=20;
geomCockpit.vertices[5].y-=10;
geomCockpit.vertices[5].z-=20;
geomCockpit.vertices[6].y+=30;
geomCockpit.vertices[6].z+=20;
geomCockpit.vertices[7].y+=30;
geomCockpit.vertices[7].z-=20;

var cockpit = new THREE.Mesh(geomCockpit, matCockpit);
cockpit.castShadow = true;
cockpit.receiveShadow = true;
this.mesh.add(cockpit);

This is an example of how to manipulate a shape to adjust it for our needs.

If you look at the complete code of the airplane, you will see a couple of more objects like a window and a better looking propeller. Nothing complicated. Try adjusting the values to get a feel for it and make your own version of the plane.

But Who is Flying the Plane?

Adding a pilot to our airplane is just as easy as adding a couple of boxes.

But we don’t just want any pilot, we want a cool pilot with windblown, animated hair! It seems like a complicated endeavor, but since we are working on a low-poly scene it becomes a much easier task. Trying to be creative to simulate fluttering hair with only a few boxes will also give a unique touch to your scene.

Animated3DScene_hair

Let’s see how it’s coded:


var Pilot = function(){
	this.mesh = new THREE.Object3D();
	this.mesh.name = "pilot";
	
	// angleHairs is a property used to animate the hair later 
	this.angleHairs=0;

	// Body of the pilot
	var bodyGeom = new THREE.BoxGeometry(15,15,15);
	var bodyMat = new THREE.MeshPhongMaterial({color:Colors.brown, shading:THREE.FlatShading});
	var body = new THREE.Mesh(bodyGeom, bodyMat);
	body.position.set(2,-12,0);
	this.mesh.add(body);

	// Face of the pilot
	var faceGeom = new THREE.BoxGeometry(10,10,10);
	var faceMat = new THREE.MeshLambertMaterial({color:Colors.pink});
	var face = new THREE.Mesh(faceGeom, faceMat);
	this.mesh.add(face);

	// Hair element
	var hairGeom = new THREE.BoxGeometry(4,4,4);
	var hairMat = new THREE.MeshLambertMaterial({color:Colors.brown});
	var hair = new THREE.Mesh(hairGeom, hairMat);
	// Align the shape of the hair to its bottom boundary, that will make it easier to scale.
	hair.geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0,2,0));
	
	// create a container for the hair
	var hairs = new THREE.Object3D();

	// create a container for the hairs at the top 
	// of the head (the ones that will be animated)
	this.hairsTop = new THREE.Object3D();

	// create the hairs at the top of the head 
	// and position them on a 3 x 4 grid
	for (var i=0; i<12; i++){
		var h = hair.clone();
		var col = i%3;
		var row = Math.floor(i/3);
		var startPosZ = -4;
		var startPosX = -4;
		h.position.set(startPosX + row*4, 0, startPosZ + col*4);
		this.hairsTop.add(h);
	}
	hairs.add(this.hairsTop);

	// create the hairs at the side of the face
	var hairSideGeom = new THREE.BoxGeometry(12,4,2);
	hairSideGeom.applyMatrix(new THREE.Matrix4().makeTranslation(-6,0,0));
	var hairSideR = new THREE.Mesh(hairSideGeom, hairMat);
	var hairSideL = hairSideR.clone();
	hairSideR.position.set(8,-2,6);
	hairSideL.position.set(8,-2,-6);
	hairs.add(hairSideR);
	hairs.add(hairSideL);

	// create the hairs at the back of the head
	var hairBackGeom = new THREE.BoxGeometry(2,8,10);
	var hairBack = new THREE.Mesh(hairBackGeom, hairMat);
	hairBack.position.set(-1,-4,0)
	hairs.add(hairBack);
	hairs.position.set(-5,5,0);

	this.mesh.add(hairs);

	var glassGeom = new THREE.BoxGeometry(5,5,5);
	var glassMat = new THREE.MeshLambertMaterial({color:Colors.brown});
	var glassR = new THREE.Mesh(glassGeom,glassMat);
	glassR.position.set(6,0,3);
	var glassL = glassR.clone();
	glassL.position.z = -glassR.position.z

	var glassAGeom = new THREE.BoxGeometry(11,1,11);
	var glassA = new THREE.Mesh(glassAGeom, glassMat);
	this.mesh.add(glassR);
	this.mesh.add(glassL);
	this.mesh.add(glassA);

	var earGeom = new THREE.BoxGeometry(2,3,2);
	var earL = new THREE.Mesh(earGeom,faceMat);
	earL.position.set(0,0,-6);
	var earR = earL.clone();
	earR.position.set(0,0,6);
	this.mesh.add(earL);
	this.mesh.add(earR);
}

// move the hair
Pilot.prototype.updateHairs = function(){
	
	// get the hair
	var hairs = this.hairsTop.children;

	// update them according to the angle angleHairs
	var l = hairs.length;
	for (var i=0; i<l; i++){
		var h = hairs[i];
		// each hair element will scale on cyclical basis between 75% and 100% of its original size
		h.scale.y = .75 + Math.cos(this.angleHairs+i/3)*.25;
	}
	// increment the angle for the next frame
	this.angleHairs += 0.16;
}

Animated3DScene_hairstyle

Now to make the hair move, just add this line to the loop function:

airplane.pilot.updateHairs();

Making Waves

You have probably noticed that the sea doesn’t really look like a sea, but more like a surface that was flattened by a steamroller.

It needs some waves. This can be done by combining two techniques we have used earlier:

  • Manipulating the vertices of a geometry like we did with the cockpit of the plane.
  • Applying a cyclic movement to each vertex like we did to move the hair of the pilot.

To make waves we will rotate each vertex of the cylinder around its initial position, by giving it a random speed rotation, and a random distance (radius of the rotation). Sorry, but you’ll also need to use some trigonometry here!

Animated3DScene_sea_manipulation

Let’s make a modification to the sea:

Sea = function(){
	var geom = new THREE.CylinderGeometry(600,600,800,40,10);
	geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));

	// important: by merging vertices we ensure the continuity of the waves
	geom.mergeVertices();

	// get the vertices
	var l = geom.vertices.length;

	// create an array to store new data associated to each vertex
	this.waves = [];

	for (var i=0; i<l; i++){
		// get each vertex
		var v = geom.vertices[i];

		// store some data associated to it
		this.waves.push({y:v.y,
										 x:v.x,
										 z:v.z,
										 // a random angle
										 ang:Math.random()*Math.PI*2,
										 // a random distance
										 amp:5 + Math.random()*15,
										 // a random speed between 0.016 and 0.048 radians / frame
										 speed:0.016 + Math.random()*0.032
										});
	};
	var mat = new THREE.MeshPhongMaterial({
		color:Colors.blue,
		transparent:true,
		opacity:.8,
		shading:THREE.FlatShading,
	});

	this.mesh = new THREE.Mesh(geom, mat);
	this.mesh.receiveShadow = true;

}

// now we create the function that will be called in each frame 
// to update the position of the vertices to simulate the waves

Sea.prototype.moveWaves = function (){
	
	// get the vertices
	var verts = this.mesh.geometry.vertices;
	var l = verts.length;
	
	for (var i=0; i<l; i++){
		var v = verts[i];
		
		// get the data associated to it
		var vprops = this.waves[i];
		
		// update the position of the vertex
		v.x = vprops.x + Math.cos(vprops.ang)*vprops.amp;
		v.y = vprops.y + Math.sin(vprops.ang)*vprops.amp;

		// increment the angle for the next frame
		vprops.ang += vprops.speed;

	}

	// Tell the renderer that the geometry of the sea has changed.
	// In fact, in order to maintain the best level of performance, 
	// three.js caches the geometries and ignores any changes
	// unless we add this line
	this.mesh.geometry.verticesNeedUpdate=true;

	sea.mesh.rotation.z += .005;
}

Animated3DScene_waves-from-cylinder

Like we did for the hair of the pilot, we add this line in the loop function:

sea.moveWaves();

Now, enjoy the waves!

Refining the Lighting of the Scene

In the first part of this tutorial, we have already set up some lighting. But we would like to add a better mood to the scene, and make the shadows softer. To achieve that we are going to use an ambient light.

In the createLights function we add the following lines:

// an ambient light modifies the global color of a scene and makes the shadows softer
ambientLight = new THREE.AmbientLight(0xdc8874, .5);
scene.add(ambientLight);

Do not hesitate to play with the color and intensity of the ambient light; it will add a unique touch to your scene.

A Smoother Flight

Our little plane already follows the mouse movements. But it doesn’t really feel like real flying. When the plane changes its altitude it would be nice if it changed its position and orientation more smoothly. In this final bit of the tutorial we will implement exactly that.

An easy way to do that would be to make it move to a target by adding a fraction of the distance that separates it from this target in every frame.

Basically, the code would look like this (this is a general formula, don’t add it to your code right away):

currentPosition += (finalPosition - currentPosition)*fraction;

To be more realistic, the rotation of the plane could also change according to the direction of the movement. If the plane goes up very fast, it should quickly rotate counterclockwise. If the planes moves down slowly, it should rotate slowly in a clockwise direction.
To achieve exactly that, we can simply assign a proportional rotation value to the remaining distance between the target and the position of the plane.

In our code, the updatePlane function needs to look as follows:

function updatePlane(){
	var targetY = normalize(mousePos.y,-.75,.75,25, 175);
	var targetX = normalize(mousePos.x,-.75,.75,-100, 100);
	
	// Move the plane at each frame by adding a fraction of the remaining distance
	airplane.mesh.position.y += (targetY-airplane.mesh.position.y)*0.1;

	// Rotate the plane proportionally to the remaining distance
	airplane.mesh.rotation.z = (targetY-airplane.mesh.position.y)*0.0128;
	airplane.mesh.rotation.x = (airplane.mesh.position.y-targetY)*0.0064;

	airplane.propeller.rotation.x += 0.3;
}

Now the plane movement looks much more elegant and realistic. By changing the fraction values, you can make the plane respond faster or slower to the mouse movement. Have a try and see how it changes.

Have a look at this final stage of our scene: Demo of part 2

Well done!

Where to Go From Here?

If you have followed until here, you’ve learned some basic, yet versatile techniques in Three.js that will enable you to create your first scenes. You know now how to create objects from primitives, how to animate them and how to set the lighting of a scene. You’ve also seen how to refine the look and movement of your objects and how to tweak ambient light.

The next step, which is out of the scope of this tutorial as it involves some more complex techniques, would be to implement a game where concepts like collisions, point collection and level control take place. Download the code and have a look at the implementation; you will see all the concepts you’ve learned so far and also some advanced ones that you can explore and play with. Please note that the game is optimized for desktop use.

Animated3DScene_TheAviatorGame

Hopefully, this tutorial helped you get familiar with Three.js and motivated you to implement something on your own. Let me know about your creations, I would love to see what you do!

Karim Maaloul

Creative Director and co-founder of EPIC Agency. Web designer, illustrator, coder and children books author.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 79

Comments are closed.
  1. Love this tutorial. I may adapt it for a student club at the university I teach at here in S. Korea. I will contact you with student variations if so!
    Thanks a million!

  2. Hi there! awesome work!! can we just use the code in our websites or are there any copyrights restrictions to it ???

  3. Thank you so much for this tutorial!!! I had a lot of fun and learned a ton. Hope to see more in the future!

  4. This was fantastic!! I have been wanting something that put it all together — and explained it so well! Beautiful tutorial! So excited by it I used it to create a quick little holiday card for my company: https://goo.gl/t2a0Rq
    Turned the aviator into a character called Major Mims (based on the CEOs actual pilot dad) and how he flys after Santa’s sleigh to catch the packages that fall out while avoiding the ice shards from the storm that’s raging! Hope I changed it enough for you the base of it is still there tho and I appreciate that foundation work! THANK YOU! Will tweet it with an @ to you as well! LOVE YOUR WORK!

  5. Those feelings when you thought you were good with JavaScript, and then that guy with Three.js or WebGL knowledge comes in the run. JavaScript? Me? No… no no, not at all!

    As someone who’s worked in both 3D (C4D/3DSM) and JavaScript — this is amazing. Great work!

  6. I always wanted to dive into three.js and this tutorial is just amazing – you are so talented, great work, bravo!

  7. Excelent tutorial. thanks for teaching .. how to load 3dmax object into threejs such as airplane

  8. This is incredible! Thanks so much for taking the time to do this. I also looked at your profile, and was amazed by how talented an illustrator you are.
    You should add a ‘Buy me a coffee’ link to this page. I would get you one for sure!

  9. https://teste-572ec.firebaseapp.com

    I am doing a fork, with other characters sound etc … I am very doubtful I tried for a degrader without success, but the sound already was ..

    body {
    position: absolute;
    width: 100%;
    height: 100%;
       background: -webkit-gradient (linear, left bottom, left top, from (# 4b6cb7), to (# 000000));
       background: linear-gradient (to top, # 4b6cb7, # 000000);
       height: 100%;
       position: inherit;
       z-index: 1;
    }