Introduction to three-dimensional Javascript with three.js
If you are already familiar with Javascript and look into the direction of a world of 3D, three.js is a way to go – it lets you quickly dive into the world of meshes, vectors and physical equations within web browser.
three.js is an abstraction layer over WebGL. It provides base classes that could be used to create advanced interactive games and simulations with less effort as it would’ve been with WebGL. three.js renders inside HTML Canvas element, just as WebGL does.
As a framework, it has a wide community that provides us lots of helpful extensions which could be used to develop faster.
Support for WebGL (and therefore three.js)
WebGL is supported in all modern browsers – even IE11 supports it.
What’s inside?
The most important parts of this framework are
- A renderer
- A scene
- A camera
- Objects present on scene
Renderer is an engine that displays created scene. It uses WebGL directly.
Scene is where everything happens – just like a stage in theater.
Camera is a point from which we observe scene, with direction and other viewing properties.
Objects are everything that is added to the scene.
Terminology
Firstly, let’s brighten things up a bit. Terminology used in world of 3D is not self-explanatory, and some theoretical knowledge is very helpful in further learning stages.
Vector – a geometric object that has magnitude (or length) and direction. Vector can be pictured as arrow, and in context of three.js is a very common construction in use that defines mesh geometry.
const vector = new THREE.Vector3(1, 2, 3);
Geometry – A skeleton that holds object shape and contains an array of Vectors as its backbone.
const geometry = new THREE.Geometry();
geometry.vertices.push(new THREE.Vector3(-10, 10, 0);
geometry.vertices.push(new THREE.Vector3(20, 10, 0));
Material – A skin describing appearance of objects in a renderer-independent way.
const material = new THREE.MeshNormalMaterial();
Mesh – Geometry with material together.
const mesh = new THREE.Mesh(geometry, material);
Practical example
Let’s create basic three.js project focusing on the most important parts. In this example I’ll be using Javascript ES6. Assume that there is already a HTML file with index.js included with single <canvas> element.
At first stage, let’s build a simple shape – a cone, just like in the picture below. It will serve as a building block of something we will build next.
Get three.js from npm
npm i three
Import the library
import * as THREE from 'three';
You can also import specific classes, and use them directly, but mostly on the web you will see syntax with usage of global THREE namespace. Let’s stick to the common way.
Set up renderer
const canvasElement = document.querySelector('canvas');
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: canvasElement // tell renderer where to render
});
renderer.setSize(window.innerWidth, window.innerHeight);
Renderer is also responsible for managing all display effects, such as antialiasing. Turning it on makes object edges smoother.
Create scene
const scene = new THREE.Scene();
Create camera and set its position
const camera = new THREE.PerspectiveCamera(
70, // sets a field of view to 70 degrees
window.innerWidth / window.innerHeight, // makes camera see in right proportions
0.01, // how close camera will see objects
20 // how far camera will see objects
);
camera.position.z = 3;
Per default camera it is directed at point (0,0,0) of a scene. We also move it a bit without changing the viewing direction.
As you see, strange magic numbers start to appear in the example above. Their meaning is relative – setting camera far away together with scaling objects to size of a skyscraper would still make the same effect as it would be smaller and closer. There is no rule how big and how small those numbers should be – just try to not to reach enormously small ones.
Also, this is the place X, Y, Z axes come into play first time. three.js uses right-handed coordinate system. What it means is that if X-axis can be called ‘width’ that grows into right, Y-axis would be the ‘height’ and Z-axis the ‘depth’ that grows towards us. In the example above, we are moving camera away from the center of the scene and a little bit up.
Create example geometry and material
const geometry = new THREE.ConeGeometry(
2, // radius
3, // height
6 // number of angles in base
);
const material = new THREE.MeshNormalMaterial();
ConeGeometry is a geometry with pre-defined vertices creating a cone. A MeshNormalMaterial is a material that maps the direction (vector) of a geometry to RGB colors. As soon as geometry is not rotating, this material stays in the same color.
Create a mesh and add it to the scene
const cone = new THREE.Mesh(geometry, material);
scene.add(cone);
Render the scene
renderer.render(scene, camera)
At this point you are able to see a scene and a created object.
Make things move
Movement is essential in 3D simulations. To do this, we’ll update mesh position inside function that calls itself by built-in Javascript function window.requestAnimationFrame(). Using this one instead of window.setInterval() allows you to match screen native frame rate and freeze rendering while tab is inactive. These are serious performance improvements that make it the suggested way to animate anything in Javascript. Always, while animating an element use window.requestAnimationFrame() instead of window.setInterval().
(function update() {
// assuming browser FPS is 60, the cone rotates 0.6rad ≈ 34° per second
cone.rotation.y += 0.01;
// after each change new frame is rendered
renderer.render(scene, camera);
// what in correlation with requestAnimationFrame creates fluid movement
window.requestAnimationFrame(update);
}());
This is an IIFE, what means that the function above gets invoked as soon as it is defined. From that point, window.requestAnimationFrame() will manage how fast the update function will be invoked in sync with GPU.
Building a pine tree
Basing on a cone we created before, it’s quite easy to build shape of a tree.
Here’s what we’ll build:
Change material
What makes color of a mesh, is a material. Pines are green, so let’s redefine it as a green one:
const material = new THREE.MeshLambertMaterial({color: 0x063e1d});
Here we use a different material – MeshLambertMaterial. It’s reflective and needs a source of light to be visible.
Set up and add light
const light = new THREE.DirectionalLight(
0x404040, // color of the light
10 // intensity
);
light.position.x = 2;
light.position.y = 5;
scene.add(light);
DirectionalLight throws light in certain direction. It allows meshes with light-dependent materials to be visible and throw shadows.
Create a pine tree
function generateTree({levels}) {
function getScaledCone(cone) {
const newCone = cone.clone();
newCone.scale.x += 0.2;
newCone.scale.y += 0.2;
newCone.scale.z += 0.2;
newCone.position.y -= 0.6;
return newCone;
}
const pineTree = new THREE.Group();
let cone = new THREE.Mesh(geometry, material);
while(levels) {
cone = getScaledCone(cone); // clone previous cone, scale it
pineTree.add(cone); // and add it to container
levels -= 1;
}
return pineTree;
}
To create a pine tree, we clone our cone multiple times.
This function returns a THREE.Group that with meshes that create our tree. THREE.Group is a container for objects – It allows you to make higher-order objects in a clean way. The cones grow to the bottom. In the generated tree, each level is a cone that is 20% larger than the one from previous level.
Create a tree and add it to the scene
const tree = generateTree({levels: 6});
scene.add(tree);
Due to the fact that the tree grows, it quickly goes out of range of our camera. To see tree from a good angle, let’s move camera a little bit.
camera.position.z = 7;
camera.position.y = -3;
Add rotation to the tree
tree.rotation.y += 0.01;
Voilà! Our tree looks nice and rotates smoothly.
Full working code and a demo can be found in the repository under this link
https://github.com/tondi/3d-javascript-with-three-js
Some fast improvements could feature
- adding a trunk
- creating a forest of trees
- introducing camera movement
Feel free to experiment!
Summary
Example above shows the basic parts of three.js. Using this knowledge, already lots of things can be created. It’s recommended to do so – just imagine an object in 3D and implement it in three.js. It is perfect starting point into world of 3D. As you learn things here, they often reoccur in other Graphic Libraries.
The theoretical knowledge and terminology given may seem hard at first, however when you get used to it, things get clear. 3D development has amazing possibilities to teach OOP faster than conventional front-end does as it operates on representations of physical objects and their relations.