Three.js for beginers

Pavel Laptev
8 min readNov 19, 2017

--

Hello, guys! Today I want to offer you my old article about three.js (I finally found time to translate it).

What we will do?

Why three.js? First of all, it’s very legit WebGL library with strong community and a lot of good examples. Also, three.js has understandable structure and wide opportunities for adjustment.

So, if you are just a designer I urge you do not afraid of fancy codes-shmodes. Think about the library as if it’s a lego constructor — prepared base, prepared pieces — all you need it’s just put them together. And if you don’t know how to do something — examples, tutorials and whole stackoverflow at your disposal.

We will make a simple scene — will go through all basic things like creating simple geometry, animation, cloning by loop, 3d-models import, raycasting and mouse events.

Step 0. Structure

Before we start, we need to come up with understanding how three.js works. The structure of three.js looks like a tree — basically it’s a node system and looks like that:

Simple enough?

Step 1. Scene

First of all include three.js into your document.

All objects three.js translate into <CANVAS>. We will create a <DIV> where we put the <CANVAS>.

<div id="container"></div>

Now JS part. I always set console.clear() to keep my console clear with each page reload, and I could knew current bugs.

console.clear();

Before we start to build a scene we should declare global variables. Later we will add a couple of new vars.

var renderer, scene, camera, distance, raycaster, projector;//get our <DIV> container
var container = document.getElementById('container');
// Helper var wich we will use as a additional correction coefficient for objects and camera
var distance = 400;

Now let’s write init() function. In the function we will build and set the whole scene — camera, lights, objects.

function init() {
//init render
renderer = new THREE.WebGLRenderer({antialias: true});
//render window size
renderer.setSize(window.innerWidth, window.innerHeight);
//background color
renderer.setClearColor(0x140b33, 1);
//append render to the <DIV> container
container.appendChild(renderer.domElement);

//init scene, camera and camera position
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.2, 25000);
camera.position.set(100, -400, 2000);
//adding camera to the scene
scene.add(camera);

//LIGHTNING
//first point light
light = new THREE.PointLight(0xffffff, 1, 4000);
light.position.set(50, 0, 0);
//the second one
light_two = new THREE.PointLight(0xffffff, 1, 4000);
light_two.position.set(-100, 800, 800);
//And another global lightning some sort of cripple GL
lightAmbient = new THREE.AmbientLight(0x404040);
scene.add(light, light_two, lightAmbient);


//OBJECTS
//here we add objects by functions which we will write below
createSpheres();
createDiamond();
createSpace();

//adding scene and camera to the render
renderer.render(scene, camera);
};

Step 2. Objects

After init() function in which we added scene and render but also we added functions for objects. Now we need to write them.

We will add three types of objects:

  1. Spheres for crowd
  2. Diamonds which will be react on click events
  3. Flat circles on the background which will bring effect of deep for the scene

Also, we will use another trick — randomly set polygons amount, size and position.

function createSpheres() {
//create empty 3d object — group for future spheres
spheres = new THREE.Object3D();
//randomly create 80 spheres by loop
for (var i = 0; i < 80; i++) {
var sphere = new THREE.SphereGeometry(4, Math.random() * 12, Math.random() * 12);
var material = new THREE.MeshPhongMaterial({
color: Math.random() * 0xff00000 - 0xff00000,
shading: THREE.FlatShading,
})

//creating the mesh and add primitive and material
var particle = new THREE.Mesh(sphere, material);
//randomly set position and scale
particle.position.x = Math.random() * distance * 10;
particle.position.y = Math.random() * -distance * 6;
particle.position.z = Math.random() * distance * 4;
particle.rotation.y = Math.random() * 2 * Math.PI;
particle.scale.x = particle.scale.y = particle.scale.z = Math.random() * 12 + 5;
//add particle to the spheres group
spheres.add(particle);
}

//correct spheres position relative to the camera
spheres.position.y = 500;
spheres.position.x = -2000;
spheres.position.z = -100;
spheres.rotation.y = Math.PI * 600;
//add spheres to the scene
scene.add(spheres);

Important and general part of any 3D library it’s import of external models.

In the beginning I wanted to create diamond by code but it turned out to be difficult — a lot of lines of code. That is why I decided to import a model. Three.js has many ways to achieve that but I choose import through JSON file.

You could create a model in any 3D editor which I made in Cinema 4d. After I imported model into ThreeJS editor. ThreeJS editor has convenient button “publish” it export a model as JSON file. But when I opened JSON it was sooooooo long.

I start to find another way, and found that Blender also has special exporter for three.js. And actually it showed himself very well.

More about export:

  1. Presentation here from Yomotsu
  2. Tutorial from Jonathan Petitcolas

Next what you need to do is upload your JSON to a server. I used gitHub for this purpose — https://raw.githubusercontent.com/PavelLaptev/test-rep/master/threejs-post/diamond.json

Now let’s write a function for diamonds, it will be a clone of the createShapes() but with slight changes.

function createDiamond() {
//create a group container
diamondsGroup = new THREE.Object3D();
//setting up loader for a model
var loader = new THREE.JSONLoader();
//load model and clone it
loader.load('https://raw.githubusercontent.com/PavelLaptev/test-rep/master/threejs-post/diamond.json', function(geometry) {
for (var i = 0; i < 60; i++) {
var material = new THREE.MeshPhongMaterial({
color: Math.random() * 0xff00000 - 0xff00000,
shading: THREE.FlatShading
});
var diamond = new THREE.Mesh(geometry, material);
diamond.position.x = Math.random() * -distance * 6;
diamond.position.y = Math.random() * -distance * 2;
diamond.position.z = Math.random() * distance * 3;
diamond.rotation.y = Math.random() * 2 * Math.PI;
diamond.scale.x = diamond.scale.y = diamond.scale.z = Math.random() * 50 + 10;
diamondsGroup.add(diamond);
}

diamondsGroup.position.x = 1400;
scene.add(diamondsGroup);

//we will delete this line later
renderer.render(scene, camera);
});
};

Last in the line — background consisting of dots. Many dots with different polygons amount and colours. Also, we change sphere geometry to circle geometry which is better due to it has less amount of polygons. In addition change Phong shading to Basic. Basic shading could show only flat colour or wireframe and it’s okay for us because on the background we don’t need shadows or high-poly objects with this approach we saving computer memory.

Despite these tiny changes everything here will be the same as in previous two functions:

function createSpace() {

dots = new THREE.Object3D();

for (var i = 0; i < 420; i++) {
var circleGeometry = new THREE.SphereGeometry(2, Math.random() * 5, Math.random() * 5);
//change meterial
var material = new THREE.MeshBasicMaterial({
color: Math.random() * 0xff00000 - 0xff00000,
shading: THREE.FlatShading,
})
var circle = new THREE.Mesh(circleGeometry, material);
material.side = THREE.DoubleSide;

circle.position.x = Math.random() * -distance * 60;
circle.position.y = Math.random() * -distance * 6;
circle.position.z = Math.random() * distance * 3;
circle.rotation.y = Math.random() * 2 * Math.PI;
circle.scale.x = circle.scale.y = circle.scale.z = Math.random() * 6 + 5;
dots.add(circle);
}

dots.position.x = 7000;
dots.position.y = 900;
dots.position.z = -2000;
dots.rotation.y = Math.PI * 600;
dots.rotation.z = Math.PI * 500;

scene.add(dots);
};

And at the moment result should be looks like that:

Step 3. Events

We have spheres, diamonds and the background. But we don’t have any related events. Next we will do:

  • Canvas resize
  • Animation for spheres and diamonds
  • Camera movement related with mouse events
  • Raycasting — analogue of mouseclick, hover etc.
  • Open links on mouse click

Canvas resize

Now <canvas> has fixed size — installed only once on the load. In order to solve the problem, we should include window listener onResize. In the end of init() function, we write

window.addEventListener('resize', onWindowResize, false);

Outside init()

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

Movement camera behind the mouse / parallax effects

In the end of init()

document.addEventListener('mousemove', onMouseMove, false);
};

And function onMouseMove()

function onMouseMove(event) {
mouseX = event.clientX - window.innerWidth / 2;
mouseY = event.clientY - window.innerHeight / 2;
camera.position.x += (mouseX - camera.position.x) * 0.005;
camera.position.y += (mouseY - camera.position.y) * 0.005;
//set up camera position
camera.lookAt(scene.position);
};

requestAnimationFrame()

Objects animation including camera and lights animated by redrawing the whole canvas with certain interval. In modern JS cool guys using requestAnimationFrame()

function animate() {
requestAnimationFrame(animate);
render();
};

We set up animate() function after init(). Animation will be inside render() and updating every time in animate() foo.

function render() {
renderer.render(scene, camera);
}

Objects animation

We already wrote the render() function, now we could add some objects animation inside

function render() {
//original example from here https://github.com/mrdoob/three.js/blob/master/examples/webgl_lights_pointlights.html
var timer = 0.00001 * Date.now();
//come up with random animation
for (var i = 0, l = diamondsGroup.children.length; i < l; i++) {
var object = diamondsGroup.children[i];
object.position.y = 500 * Math.cos(timer + i);
object.rotation.y += Math.PI / 500;
}
for (var i = 0, l = spheres.children.length; i < l; i++) {
var object = spheres.children[i];
object.rotation.y += Math.PI / 60;
if (i < 20) {
object.rotation.y -= Math.PI / 40;
}
}
//add in the render
renderer.render(scene, camera);
};

Mouse events

Unlike 2d world 3d has their own rules. That is why standard DOM events don’t work. In 3d world we have analogue well known as raycasting (example here https://threejs.org/examples/#webgl_interactive_cubes). Raycasting means that we are projecting ray through the scene and indicate object when object and ray intersect.

Also there is another good tutorial https://soledadpenades.com/articles/three-js-tutorials/object-picking/

So, we want to detect mouse hover on diamonds and add emissive shading to the material. On order to achieve that let’s write a few new vars in the very beginning of the code

var raycaster = new THREE.Raycaster(),INTERSECTED;
var mouse = new THREE.Vector2();

And in addition to mousemove include this code

mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

Go to the render() foo

//update raycaster with mouse movement  
raycaster.setFromCamera(mouse, camera);
// calculate objects intersecting the picking ray
var intersects = raycaster.intersectObjects(diamondsGroup.children);
//count and look after all objects in the diamonds group
if (intersects.length > 0) {
if (INTERSECTED != intersects[0].object) {
if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
INTERSECTED = intersects[0].object;
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
//setting up new material on hover
INTERSECTED.material.emissive.setHex(Math.random() * 0xff00000 - 0xff00000);
}
} else {
if (INTERSECTED) INTERSECTED.material.emissive.setHex(INTERSECTED.currentHex);
INTERSECTED = null;
}

Click on a object — open a link

Here we need also add a new listener —” mousedown”.

document.addEventListener('mousedown', onDocumentMouseDown, false);

This listener will be watching for click events— the same function as hover, but with a few new lines of code.

function onDocumentMouseDown(event) {    event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera); var intersects = raycaster.intersectObjects(diamondsGroup.children);
if (intersects.length > 0) {
//get a link from the userData object
window.open(intersects[0].object.userData.URL);
}
};

What is it — userData? It’s a JS object which contains URL links.

Create it inside createDiamond() foo.

//create array with any links
var arr = ['https://link',
'https://link',
'https://link',
'https://link'];
//and randomly push it into userData object
diamond.userData = {
URL: arr[Math.floor(Math.random() * arr.length)]
};

Thank is all folks! Thank you for reading :-)

--

--

Pavel Laptev
Pavel Laptev

Responses (8)