Getting Started with Three.js

Three.js: Illumination Confirmed!

3D graphics in the browser have been a hot topic ever since they were first introduced. However, creating apps in pure WebGL takes ages. This is why libraries have appeared, Three.js being one of the most popular. It is a nice and simple layer on top of WebGL, that provides its users with plenty of well-written documentation.

Three.js may look complex at first, but it takes even more coding to write the same program in pure WebGL, mostly to write 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 a 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 over at www.threejs.org, demonstrate this. 
You can easily draw inspiration from these creative demos and create your own games based on them.

Many 3D WebGL projects have spun off and are propelling Three.js engine adoption. Two of these projects are https://play.freeciv.org/ and https://clara.io/.

We’ll create a scene where the user clicks away colorful blocks in an endless jumble while keeping track of the number of times the cubes were clicked. You can find a working example here.

Setting up Three.js

To install three.js you can click on the download button at https://threejs.org. Once the zip has finished downloading, open it up 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 with the three.min.js file.

Programming our game

To demonstrate how the game was built, we will start by our global variables, followed by the functions used to set up our cubes, header, scene, and score, followed by event listeners, and finally how they are all used 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 the number of cubes we want. From there we have the container itself and stats. The container will have all the cubes, title, and stats. It represents the whole game scene. Stats will be used to 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, 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 due to the 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="http://jscrambler.com" target="_blank">Jscrambler</a> - Three.js cube example';
container.appendChild(info);
}

Here, we can see our setHeader function. It’s just a simple text linking 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 the renderer, followed by 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 mouse. The reason behind this is that the onMouseDownPosition is used to rotate the Camera, while 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 suffer a progressive rotation and position change. The count is also incremented, and our scoring element will be altered as well, according to the new count value.

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 isMouseDownvariable 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 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 to check out the final example here.

Remember that if you have a Three.js game or any other Javascript application you want to protect from prying eyes, Jscrambler is a good 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. 
You can test Jscrambler and all of its functions in the Playground app at https://app.jscrambler.com/dashboard.


Originally published on blog.jscrambler.com on April 20, 2017.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.