August 10, 2021
(Updated: September 24, 2021)
Download the Three.js Webpack Starter
We need 4 elements to get started:
1.
A scene that will contain objects
2.
Some objects
3.
A camera
4.
A renderer
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:
// Sceneconst scene = new THREE.Scene()
Important: If you don't add your objects to the scene you won't be able to see them.
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).
// Objectconst geometry = new THREE.BoxGeometry(1, 1, 1)const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
We combine these to create the final mesh
// Objectconst geometry = new THREE.BoxGeometry(1, 1, 1)const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })const mesh = new THREE.Mesh(geometry, material)
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
// Sizesconst sizes = {width: 800,height: 600}// Move the Camera backwards so we can see our scenecamera.position.z = 3// Cameraconst 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.
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 DOMconst canvas = document.querySelector('canvas.webgl')// Rendererconst 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)
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)
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.
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')
Also expresses rotation but in a more mathematical way, which solves the order problem. When we update rotation
we also update the objects quaternion
.
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 = 1group.scale.y = 2group.rotation.y = 1scene.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);
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 objectsmesh.rotation.y += 0.01// Renderrenderer.render(scene, camera)// Call tick again on the next framewindow.requestAnimationFrame(tick)}tick()
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 = () =>{// Timeconst currentTime = Date.now()const deltaTime = currentTime - timetime = currentTime// Update objectsmesh.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 objectsmesh.position.x = Math.cos(elapsedTime)mesh.position.y = Math.sin(elapsedTime)}tick()
Do not use .getDelta()
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}
// Remove the margin and padding on the HTML document* {margin: 0;padding: 0;}// Prevent any overflow scrollhtml,body {overflow: hidden;}// We need to fill the whole space.webgl {position: fixed;top: 0;left: 0;outline: none;}
window.addEventListener('resize', () => {// Update sizessizes.width = window.innerWidthsizes.height = window.innerHeight// Update cameracamera.aspect = sizes.width / sizes.heightcamera.updateProjectionMatrix()// Update rendererrenderer.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.
Using the dblclick
event we can toggle fullscreen mode
window.addEventListener('dblclick', () => {if(!document.fullscreenElement) {canvas.requestFullscreen()}else {document.exitFullscreen()}})
Table of Contents
Like the content I'm creating? Show some love and:
Buy me a Coffee