Getting Started with Three.js

By Júlia Rocha 9 min read

Three.js: Illumination Confirmed!

3D graphics in the browser have been a hot topic since their introduction. However, creating apps in pure WebGL takes ages.

That is why libraries have emerged, with Three.js as one of the most popular. It is a simple layer on top of WebGL that provides plenty of well-written documentation.

Three.js may look complex at first, but writing the same program in pure WebGL takes even more coding, mostly to build a rendering engine. With Three.js the heavy lifting is done without sacrificing much flexibility.

Three.js also does an excellent job of abstracting many of the details of WebGL, but it also gives you very clean, low-level access to all the rendering (projection, animation) capabilities.

If you want to do 3D models, textures, and render scenes that look more realistic, Three.js is the way to go.

Countless well-documented examples on the website demonstrate this. You can draw inspiration from these creative demos and build your games around them.

Many 3D WebGL projects have spun off and are propelling Three.js engine adoption.

We will create a scene where the user clicks colorful blocks in an endless jumble while tracking how many times the cubes are clicked. Find a working example.

Setting up Three.js

To install three.js, click the download button in the JavaScript 3D library.

Once the zip finishes downloading, open it and go to the build folder. Inside, you’ll find a file called three.min.js that you should copy into your local development directory. From here, you can include the library in your HTML file.

<script src="js/three.min.js"></script>

For this game, you’ll also need the following files:

  • examples/js/libs/stats.min.js
  • examples/js/renderers/CanvasRenderer.js
  • examples/js/renderers/Projector.js
  • examples/js/libs/tween.min.js

Remember to add them to the HTML file, as shown in the three.min.js file.

Programming our game

To show how the game was built, we will start with our global variables, then the functions that set up our cubes, header, scene, and score, then the event listeners, and finally how they all work together to build the game.

Global variables

We will need to use the following variables.

var numCubes = 10;
var container, stats;
var camera, scene, renderer;
var textureLoader = new THREE.TextureLoader();
var raycaster;
var mouse;
var cubeTexture = textureLoader.load('http://i.imgur.com/U1DnhNv.png');
var isMouseDown = false,
	onMouseDownPosition, onMouseDownTheta = 45,
	onMouseDownPhi = 60,
	phi = 60,
	theta = 45,
	radious = 1600,
	count = 0;

We start by defining how many cubes we want. From there, we’ll set up the container and stats.

The container will have all the cubes, titles, and stats. It represents the whole game scene. Stats will monitor the game’s performance (FPS, frames per second, and MS, milliseconds to render a frame).

We create our camera, scene, renderer, raycaster, mouse, and cube texture variables. Then we have some variables that will be used for camera rotation, a count variable used to count the number of times the cubes were clicked.

Creating a Cube

The following function demonstrates how to create cubes through BoxGeometry. To use its constructor, we have to at least define the width, height, and depth.

function createGeometry() {
	var geometry = new THREE.BoxGeometry(100, 100, 100);
	var object = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
		color: Math.random() * 0xffffff,
		opacity: .75,
		map: cubeTexture
	}));
	object.position.x = Math.random() * 800 - 500;
	object.position.y = Math.random() * 800 - 600;
	object.position.z = Math.random() * 800 - 700;
	object.scale.x = Math.random() * 2 + 1;
	object.scale.y = Math.random() * 2 + 1;
	object.scale.z = Math.random() * 2 + 1;
	object.rotation.x = Math.random() * 2 * Math.PI;
	object.rotation.y = Math.random() * 2 * Math.PI;
	object.rotation.z = Math.random() * 2 * Math.PI;
	return object;
}

Here, boxes or cubes are created, and have a color, opacity, and texture applied, followed by a random position, scale, and rotation.

This means that although all the cubes have the same texture and opacity, they’ll be in different places, with different rotations, and different scales. Some cubes will look like rectangles because of scaling.

Creating a Header, Score, and Scene

The following functions will be used later on inside the init function:

function setHeader() {
	var info = document.createElement('div');
	info.style.position = 'absolute';
	info.style.top = '10px';
	info.style.width = '100%';
	info.style.textAlign = 'center';
	info.innerHTML = '<a href="https://jscrambler.com" target="_blank">Jscrambler</a> - Three.js cube example';
	container.appendChild(info);
}

Here is our setHeader function. It’s just a simple text link to Jscrambler that says “Jscrambler – Three.js cube example”. After creating the element it is placed inside the container through container.appendChild(info).

function setScore() {
	var info = document.createElement('div');
	info.id = 'score';
	info.style.position = 'absolute';
	info.style.top = '30px';
	info.style.width = '100%';
	info.style.textAlign = 'center';
	info.innerHTML = 'Score: ' + count;
	container.appendChild(info);
}

Like in the setHeader function, setScore creates an element, this time giving it an Id, so we can later alter its inner HTML. Note that the inner HTML has our count variable, which will keep track of the number of times the cubes were clicked.

function createScene() {
	scene = new THREE.Scene();
	renderer = new THREE.WebGLRenderer({
		antialias: true
	});
	renderer.autoClear = true;
	renderer = new THREE.CanvasRenderer();
	renderer.setClearColor(0xf0f0f0);
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);
	container.appendChild(renderer.domElement);

	for (var i = 0; i < numCubes; i++) {
		var geometry = createGeometry();
		scene.add(geometry);
	}
}

As for the createScene, we have followed the creation of several cubes with var geometry=createGeometry(), according to the previously defined numCubes, and their addition to the scene.

Event Listeners

These event listeners will be triggered after being defined inside the init function.

onWindowResize

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

Here we resize our project, by altering the camera, and the renderer, whenever the window is resized.

onDocumentMouseDown

function onDocumentMouseDown(event) {
	isMouseDown = true;
	onMouseDownTheta = theta;
	onMouseDownPhi = phi;
	onMouseDownPosition.x = event.clientX;
	onMouseDownPosition.y = event.clientY;
	mouse.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1;
	mouse.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1;
	raycaster.setFromCamera(mouse, camera);
	var intersects = raycaster.intersectObjects(scene.children); //if mouse intersects a cube
	if (intersects.length > 0) {
		new TWEEN.Tween(intersects[0].object.position).to({
				x: Math.random() * 800 - 400,
				y: Math.random() * 800 - 432,
				z: Math.random() * 800 - 777
			}, 2000)
			.easing(TWEEN.Easing.Elastic.Out).start(); //moves cube
		new TWEEN.Tween(intersects[0].object.rotation).to({
				x: Math.random() * 2 * Math.PI,
				y: Math.random() * 2 * Math.PI,
				z: Math.random() * 2 * Math.PI
			}, 2000)
			.easing(TWEEN.Easing.Elastic.Out).start(); //rotates cube
		count++;
		document.getElementById("score").innerHTML = "Score: " + count;
	}

}

Once the mouse is clicked, the isMouseDown variable is set as true. Now we have Theta and Phi, which help with the camera rotation, and we record the mouse position, once in onMouseDownPosition and another time in the mouse.

The reason behind this is that the onMouseDownPosition is used to rotate the Camera, while the mouse is used with the raycaster to work out what objects the mouse is over. If the mouse is over a cube, the clicked cube will rotate and change position progressively.

The count is also incremented, and our scoring element is updated based on the new count.

onDocumentMouseMove

function onDocumentMouseMove(event) {
	if (isMouseDown) {

		theta = -((event.clientX - onMouseDownPosition.x) * 0.5) + onMouseDownTheta;
		phi = ((event.clientY - onMouseDownPosition.y) * 0.5) + onMouseDownPhi;

		phi = Math.min(180, Math.max(0, phi));

		camera.position.x = radious * Math.sin(theta * Math.PI / 360) * Math.cos(phi * Math.PI / 360);
		camera.position.y = radious * Math.sin(phi * Math.PI / 360);
		camera.position.z = radious * Math.cos(theta * Math.PI / 360) * Math.cos(phi * Math.PI / 360);
		camera.updateMatrix();
	}
}

Whenever the mouse is moved this function is triggered if the isMouseDown variable is true, that means that the mouse is being clicked, and the camera will rotate, according to the mouse movement.

onDocumentMouseUp

function onDocumentMouseUp(event) {
	isMouseDown = false;
}

This function is triggered whenever the mouse stops being clicked. The isMouseDown variable is set as false, so the program knows the mouse isn’t being clicked anymore.

Building the game

To build our game, we need to call the init() and the animate() function which we’ll define below.

init();
animate();

This will lead to:

Init

function init() {
	container = document.createElement('div');
	document.body.appendChild(container);
	setHeader();
	setScore();
	camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000);
	camera.position.y = 360;
	camera.position.z = 555;
	raycaster = new THREE.Raycaster();
	mouse = new THREE.Vector2();
	stats = new Stats();
	container.appendChild(stats.dom);

	createScene();
	onMouseDownPosition = new THREE.Vector2();
	document.addEventListener('mousedown', onDocumentMouseDown, false);
	document.addEventListener('mousemove', onDocumentMouseMove, false);
	document.addEventListener('mouseup', onDocumentMouseUp, false);
	window.addEventListener('resize', onWindowResize, false);
}

In the init function, we set our container as a div. It’ll be filled with our cubes, header, and score, as shown previously.

Then we have setHeader(); and setScore() that, as we’ve demonstrated, are used to create the header text and the score tracker of our game.

From here we set our camera’s initial position, our raycaster, mouse, and stats. We have the createScene function, which handles our renderer initialization, and cube creation.

All that’s left is handling the mouse position, with the onMouseDownPosition, and our Event Listeners for when the mouse is moved, down, or up, and when the window is resized.

Animate and Render

We need to use an animation function. It may not seem like anything is really “animated” here in the traditional sense, but we do need to redraw when the camera orbits around the cubes.

This is done by calling the render function that will alter the camera’s position, to change the way it looks at the scene.

function animate() {
	requestAnimationFrame(animate);
	render();
	stats.update();
}

function render() {
	TWEEN.update();
	theta += 0.1;
	camera.position.x = radious * Math.sin(THREE.Math.degToRad(theta));
	camera.position.y = radious * Math.sin(THREE.Math.degToRad(theta));
	camera.position.z = radious * Math.cos(THREE.Math.degToRad(theta));
	camera.lookAt(scene.position);
	renderer.render(scene, camera);
}

Camera

Looking further into detail at the camera.

	camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000);
	camera.position.y = 360;
	camera.position.z = 555;

Our camera has a perspective projection, which is designed to mimic the human eye. We start by setting its field of view, aspect ratio, near plane, and far plane. The planes represent the distance at which you start seeing objects near and far, as they originate from the camera.

In other projects, it might be more fitting to employ an Orthographic perspective like the one shown above. For now, we’ll keep the PerspectiveCamera and position it to view a good amount of blocks without them blocking the line of sight.

The vast selection of plugins and community projects built on three.js lets anyone take advantage of the API. This doesn’t only expand the three.js community but also incentivizes new spin-off projects based on this library.

There’s also a three.js extension site known as threeX which provides several handy components which you can reuse when making your app. It could be a great way to deploy rapid experiments with some visual tweaks applied through components rather than rolling your own.

Remember that if you have a Three.js game or any other Javascript application you want to protect from prying eyes, Jscrambler is the best solution to do so.

Some good transformations for games would be Code Locks, where you can lock your code to dates, browsers, operating systems, and domains, and Self Defending, to prevent your code from being debugged and tampered with.

You can test Jscrambler and all of its functions in the Playground app.