August 10, 2021

(Updated: September 24, 2021)

Threejs Journey

Showcase

Quick Start

Download the Three.js Webpack Starter

Basic Scene

We need 4 elements to get started:

  1. 1.

    A scene that will contain objects

  2. 2.

    Some objects

  3. 3.

    A camera

  4. 4.

    A renderer

Scene

The scene is like a container. You place your objects, models, particles, lights, etc. in it, and at some point, you ask Three.js to render that scene.

To create a scene, use the Scene class:

// Scene
const scene = new THREE.Scene()

Important: If you don't add your objects to the scene you won't be able to see them.

Objects

Objects can be many things. You can have primitive geometries, imported models, particles, lights, and so on.

Important: To create a cube we would need to create a type of object named Mesh. A Mesh is the combination of a geometry (the shape) and a material (how it looks).

// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })

We combine these to create the final mesh

// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)

Camera

The camera acts as a theoretical point of view. When you do a render of that scene it will be from that cameras point of view.

You can have multiple cameras just like on a movie set, and you can switch between those cameras as you please. Usually, we only use one camera.

The field of view is how large your vision angle is and is the first argument PerspectiveCamera takes. An explainer video can be found in this lesson.

By default the camera and our objects will sit in the centre of the scene. Without moving our camera or object you won't be able to see anything. We can use the position property to move the camera backwards

// Sizes
const sizes = {
width: 800,
height: 600
}
// Move the Camera backwards so we can see our scene
camera.position.z = 3
// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)

Set the aspect ratio by dividing your width value by your height value and make sure to add your camera to the scene.

Renderer

First we need to create our canvas within the DOM (the class can be whatever you like):

<canvas class="webgl"></canvas>

Now we can set up our renderer

// Select our canvas from thr DOM
const canvas = document.querySelector('canvas.webgl')
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
// Set our renderer size here (how big it will show in the viewport)
renderer.setSize(sizes.width, sizes.height)

Transform Objects

There are 4 properties to transform objects in our scene:

  • position (to move the object)

  • scale (to resize the object)

  • rotation (to rotate the object)

  • quaternion (to also rotate the object)

By default the the Y axis is going upward/downward, the X axis is left/right and the Z axis is going towards/away from us. This differs from Blender where the Z axis is up and down.

When we say 1 we are declaring a relative unit. As a mental model we can decide on a unit but in practice it makes no difference.

The position property is an instance of the Vector3 class. As well as the x, y and z properties, it also has many other useful methods: length() distanceTo() normalize()

Instead of changing x, y and z separately, we can also use the set() method:

mesh.position.set(0.7, - 0.6, 1)

Axes Helper

To help us with the axes we can create an AxesHelper:

const axesHelper = new THREE.AxesHelper(2)
scene.add(axesHelper)

The value we supply to the helper increases the length of each axes.

Rotation

The rotation property also has x, y, and z properties, but instead of a Vector3, it's a Euler. To best visualise rotation we can imagine putting a stick through the objects centre in the axis's direction and spinning the object.

The values of the axes are represented in radians. For a half rotation you have to write something like 3.14159... or π. We can write an approximation of pi by using Math.PI

To prevent Gimbal Lock we can reorder the rotation e.g mesh.rotation.reorder('YXZ')

Quaternion

Also expresses rotation but in a more mathematical way, which solves the order problem. When we update rotation we also update the objects quaternion.

Groups

We can group objects in the scene by using the Group class. Similar to Figma and how we can group layers.

const group = new THREE.Group();
group.position.y = 1
group.scale.y = 2
group.rotation.y = 1
scene.add(group);
const cube1 = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshBasicMaterial({color: 0xff0000})
)
group.add(cube1);
const cube2 = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshBasicMaterial({color: 0x00ff00})
)
cube2.position.x = -2;
group.add(cube2);
const cube3 = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshBasicMaterial({color: 0x0000ff})
)
cube3.position.x = 2;
group.add(cube3);

Animations

When using Three.js animations work similar to stop motion. You move the objects, and you render it.

Screens run at a specific frequency which we call frame rate. Most screens run at 60FPS, some run slower and some much faster. We want to move the object on each frame which is where window.requestAnimationFrame comes in.

requestAnimationFrame will execute the function you provide on the next frame. If this function also uses requestAnimationFrame then we have created our loop. Once we add a transform and render our scene our object will now animate.

const tick = () => {
// Update objects
mesh.rotation.y += 0.01
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()

Adapting to the Framerate

Due to different screens having different FPS we need to standardise our animation speed across screens. We create our deltaTime to do this, which is our currentTime - previousTime.

let time = Date.now()
const tick = () =>
{
// Time
const currentTime = Date.now()
const deltaTime = currentTime - time
time = currentTime
// Update objects
mesh.rotation.y += 0.01 * deltaTime
// ...
}
tick()

Three.js also has a built in version of this (but it's good to understand what's happening). Using Math.sin and Math.cos we can move our cube in a circle.

const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update objects
mesh.position.x = Math.cos(elapsedTime)
mesh.position.y = Math.sin(elapsedTime)
}
tick()

Do not use .getDelta()

Fullscreen and Resizing

Instead of using fixed numbers in the size variable we can use window.innerWidth and window.innerHeight

const sizes = {
width: window.innerWidth,
height: window.innerHeight
}

Add some CSS

// Remove the margin and padding on the HTML document
* {
margin: 0;
padding: 0;
}
// Prevent any overflow scroll
html,
body {
overflow: hidden;
}
// We need to fill the whole space
.webgl {
position: fixed;
top: 0;
left: 0;
outline: none;
}

Handle the Resize and Pixel Ratio

window.addEventListener('resize', () => {
// Update sizes
sizes.width = window.innerWidth
sizes.height = window.innerHeight
// Update camera
camera.aspect = sizes.width / sizes.height
camera.updateProjectionMatrix()
// Update renderer
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

When camera properties like aspect are changed we need to also update the projection matrix using camera.updateProjectionMatrix()

To prevent performance issues we limit the pixel ratio to 2 using renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) . Adding it to the resize handler accounts for users moving a window from one screen to another.

Switch to Fullscreen

Using the dblclick event we can toggle fullscreen mode

window.addEventListener('dblclick', () => {
if(!document.fullscreenElement) {
canvas.requestFullscreen()
}
else {
document.exitFullscreen()
}
})