From our sponsor: Ready to show your plugin skills? Enter the Penpot Plugins Contest (Nov 15-Dec 15) to win cash prizes!
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!
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.
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
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);
};
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.
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!
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;
}
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.
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!
Dude, crazy!
Congrats, it’s so simple but really impressive.
This is madness! I really love it! It looks so awesome!
Amazing work & tutorial, thanks a lot!
You have inspired me to create! Thank you!
Wow, That’s interesting.
wouahou!!!!
nothing more to say
Awesome!
This is awesome. Thanks for creating the tutorial!
Wow man, this is so great lol.. Really madness! so much things to learn and do
Really awesome!!!! Congrats man!
This is great! Though one thing I’d say which seems to be common, is that you’re not doing any dpi density checking when calculating the screen size, so on my 4K desktop where Windows has made everything 200% bigger, the script in its current state thinks the scene is only 1920×1080, which results in a blurry experience. document.body.clientWidth * window.devicePixelRatio gets the correct width.
Great job, great write up.
Very good tuto, the result is mindblowing !
Thank you for your work & sharing π
Support HDPI displays too <3
renderer.setPixelRatio(window.devicePixelRatio ? window.devicePixelRatio : 1);
I love his hair waving in the wind. Very impressive demo here, as always!
dude, this is awesome!
thanks for sharing, you are one of the best ever!
Wow! Just stunning. Very well done. Thanks so much for sharing.
That’s really something..Impressed…Thank you for sharing π
This is awesome! Thanks for sharing.
Just love it this Game π
Awesome work Karim
Thanks for sharing. Your code is very neat and the separation of concerns makes it a lot easier to grasp. Well done!
Brilliant!
This is so cool! Thank you!
Really nice but.. Why not give it a GUI, make all the code in little tool boxes, timeline, drawing panel..etc. Sale it (Adobe?) and call it FlashGL or GLash. Now everybody would be able to create this in 4-5 minutes. π
Hell yeah, awesome idea!! Please let me know when you finish it. I’ll be the first one to try it!
This is a really well written tutorial, I checked it out over my lunch break and was able to modify the source in a few ways because of the simplicity of three.js. Thank you for the fun learning experience π
This is an amazing concept, thanks for sharing!!
Awesome! π
so cool! TY
This couldn’t have come on a better time! Thanks for this great material.
That’s a great tutorial, thanks, I learned a lot.
But still something bugs me: in the last updatePlane function, targetX is never used, is that normal ?
Thank you Utopiad,
You’re right about the targetX. I haven’t mention it in the tutorial, but in the final game I used it to update the fov of the camera instead of the position of the airplane. Which helps the user to see more or less environment.
Ah ok, so I’ll see that later when I’ll start the game part.
Thanks for the answer !
wow! time to learn three.js it seems ^^)
Absolutely amazing tutorial Karim.
As soon as I saw the main screenshot of the project I thought: “That’s 3D low-poly design! I know who that is, Karim!”
+100 amigo.
Thank you for sharing π
thank you Ricardo! I really appreciate it !
Really nice and exceptionally executed. Such an inspiration.
I am now trying to recreate it in Unity3D for WebGL to see what the performance comparison would be and also as an exercise of how fast/easy I can implement it in Unity3D with just the standard tools at my disposal (no additional assets used)
Here’s a link to my progress so far: http://www.xna.ro/unity/aviator/
Just WOW … !
This is brilliance x2. Absolutely marvellous work!
Travail de haute voltige + excellent tuto – nice :::)
great game and explanation!
Wow…
Very Nice Game,
And Prefect Example For ThreeJs,
You Are Awesome π
I enjoyed your game. It is beautiful.
Awesome work
Wonderful work and great job walking through the process and code!
hi!
where the end of the game?
how do you know that a person won?
and how then do a redirect to win this page?
Really incredible project. At the same time has a bug π (you can go to bottom)
Awesome and relevant!!!!
omega good job!!!
just WOW~!
Hello,
Thanks for sharing nice tutorials.
Love it! Just amazing. Really awesome +1
Absolutely amazing tutorial! Five stars. Perfectly structured, amazingly split into pieces and well explained.
As someone looking into three.js, without any background in 3d, this helped me so much!
Thank you. Please make another one to demonstrate keyboard movements and some other common stuff that games use.