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.

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

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

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:

A scene: consider this as the stage where every object needs to be added in order to be rendered A camera: in this case we will use a perspective camera, but it could also be an orthographic camera. A renderer that will display all the scene using WebGL. One or more objects to render, in our case, we will create a plane, a sea and a sky (a few clouds) 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.

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.

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

create a geometry create a material pass them into a mesh 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.

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.

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:

// 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.

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; }

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.