A brief intro to ThreeJS
ThreeJS is a JavaScript library, built on top of WebGL, used for making 3D visuals on the web.
It's a great tool for a bunch of reasons:
- It's free and open-source with a large and growing community using and developing it.
- It has a well-designed, intuitive API (Application Programming Interface), and great docs.
- It uses WebGL "under the hood", so you get the perfomance benefits of the GPU without the sometimes confusing and verbose syntax of WebGL itself.
- It's 400kb, which is manageable in size (though some quibble on this), so it won't bog down your website loading time.
- It has a bustling ecosystem of tools, examples, and libraries built with it.
I personally love using it for adding 3D scenes and visuals to websites. So here's a quick primer on how to start building a ThreeJS scene from scratch.
Some of knowledge of JavaScript and HTML will be helpful but hopefully isn't necessary to follow along.
Working with ThreeJS
I'll start by assuming you have a clean web project scaffolded out with ThreeJS installed. If not, I suggest starting with a Vite template and installing ThreeJS, or starting with this ThreeJS template.
ThreeJS is a JavaScript library, so the meat of this tutorial will be creating a script (e.g. script.js), but you also need a canvas element in your HTML to display the rendered scene.
<canvas id="webglCanvas"></canvas>
Now on to the script.
The first thing to do in your ThreeJS script is to import ThreeJS itself. You can either import the parts you need or the entire core library. I usually start by importing the whole library for simplicity—you can always "treeshake" out the parts you don't need when you're ready to publish.
To import it, you can put a simple import statement like this at the top of your script:
import * as THREE from 'three';
Setting the scene
With ThreeJS imported, we can start setting up the scene.
To render anything in ThreeJS you first need to set up a renderer and camera. Then you need something to render, usually a ThreeJS scene with meshes—or 3D objects—in it.
Let's start with the renderer.
Renderer
The renderer does the heavy lifting of turning the scene you defined in code into pixels on the browser's canvas (which can happen up to 60 times a second or more).
To see anything on screen you'll have to render at least once. You do that by calling the render() method on the renderer with the ThreeJS scene (or mesh) and camera as arguments.
The renderer is created with the WebGLRenderer() constructor. It takes an object as argument with a few properties like antialias—whether to apply antialising—and alpha—whether the output should have an alpha channel, otherwise known as transparency. I'll set both of those to true.
The HTML canvas element can also be included as an argument when initializing the renderer. There should already be a canvas element in the HTML, so we can use the document.getElementById() method to grab it.
We also need to set the size and pixel ratio of the renderer. I prefer to use up the whole screen when creating a scene, so I'll set the size to be the full width and height of the window. You can access those dimensions with the window.innerWidth and window.innerHeight browser methods.
For pixel ratio, we can default to the device pixel ratio, accessed via the window.devicePixelRatio() method (but keep it at a maximum of 2 for perfomance reasons). If someone is using a HiDPI device, or a device with higher pixel density, this will prevent blurring on the final output.
Altogether, the code for creating a basic ThreeJS renderer looks like this:
const canvas = document.getElementById('webglCanvas');
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio( Math.min(window.devicePixelRatio,2));
Now let's set up a camera.
Camera
There's a few built-in camera types in ThreeJS, including Orthographic and Perspective cameras. I'll start with Perspective since it's more realistic. Orthographic makes all objects appear to be the same size irrespective of distance, which is useful for example if you're rendering an effect or shader on a 2D plane.
The ThreeJS PerspectiveCamera constructor takes field of view and aspect ratio as arguments. Field of view is how wide of an angle you can see. 90 degrees is a good starting point. As for aspect ratio, we'll start with the aspect ratio of the device screen, or screen width divided by screen height (and later on, we can add an event listener to update this if the user changes their browser window dimensions mid-scene).
The camera also takes arguments for near-view, the z-axis threshold before which objects are invisible, and far-view, the z-axis threshold beyond which objects are invisible. I set those at .01 and 105 respectively.
By default in ThreeJS, the camera starts at position (0,0,0). Since objects are also initialized at the origin, we can move the camera "back" along the z-axis so it can see the origin and any things we put there. Units are arbitrary here, but ~5 units of space between the origin (the focus of the scene) and the camera feels like a good starting point to me. So we can start the camera out at the position (0,0,-5) and it will look in the direction of (0,0,0).
Here's all the code for setting up the camera:
const camera = new THREE.PerspectiveCamera(90, window.innerWidth/window.innerHeight, .01, 105);
camera.position.set(0 , 0 , -5 );
camera.lookAt(new THREE.Vector3(0,0,0));
You can of course set the camera wherever you want—it's your world. I like (0,0,-5) because it's intuitive for me. I can imagine there's a football field in front of me and I'm standing in the middle of the endzone—like a quarterback scanning the field. Hence why I set the far threshold of the camera (where objects disappear if they go passed it) at 105, so I can see to the opposite goal line. Except I'm a really short quarterback because I left the y-value at 0.
Now we let's make the scene.
Scene
A "scene" in ThreeJS is like a placeholder for all the things we eventually want to render. These can include 3D objects (also called meshes), lights, and effects. When it's time to render—i.e. when you actually draw stuff on the canvas—the scene will get passed to the ThreeJS renderer along with the camera to render the output you see on screen.
The scene is created with the simple ThreeJS Scene constructor.
const scene = new THREE.Scene();
Adding Stuff to the scene
Next is the fun part: we can start adding stuff, or objects that takes up space in the scene.
My go to object for gut-checking a scene set-up is a simple box.
To create a box, or really any object in ThreeJS, we need two things: geometry and material.
- The geometry consists of the vertices of an object and any other relevant geometric data, like
normals(which way the geometry is facing at a given vertex) anduvs(describes how to map a texture or shader to the area in between vertices). - The material is what "covers" the geometry. It defines how the object looks—its color, texture, how it responds to light, and so on.
When we combine a material and geometry, it yields a mesh which is what is rendered on screen. Materials and geometries can be shared between different meshes to save memory and improve performance.
ThreeJS comes with an easy constructor for making a box geometry, BoxGeometry(). It takes width, height, depth, and a few other optional parameters as arguments. For simplicity, we can start with a 1x1x1 box so width, height, and depth will all be 1.
For the material, there's a range of built-in ThreeJS options that give you different types of surfaces that reflect light in slightly different ways. We can start with MeshPhongMaterial, a material that simulates shiny surfaces. Who doesn't like shiny things?
We can glue the geometry and material together using the ThreeJS Mesh() constructor, and then add the resulting mesh to the scene. The code below shows how to make the box and add it to the scene.
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshPhongMaterial();
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
scene.add(boxMesh);
We can almost see our scene with a 3D box, just one more step! We have to actually render the scene. To render the scene, we just call the render() method on the renderer created earlier, and give it the scene and camera as arguments.
renderer.render(scene, camera);
And voilá: a box!
It might seem easy—and there's a lot more we can do with ThreeJS to add spizzaz—but just getting a thing to render in 3D on a browser is an accomplishment on its own.
Resize event listener
One more thing to make the scene responsive is to resize the canvas and camera aspect ratio when the window size is changed.
We can do that by adding an event listener for when the window is resized, and using the new width of and height (i.e. window.innerWidth and window.innerHeight) to reset the size of the renderer and the camera's aspect ratio.
We also need to update the camera's projection matrix, which has to be called after any change to the camera's parameters in order for that change to take effect.
Finally, we need to re-render the scene with the updated camera and renderer (which is only necessary since we're not already updating the scene regularly in an animation loop).
That resize function ends up looking like this:
window.addEventListener('resize',() => {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
renderer.setSize(windowWidth, windowHeight);
camera.aspect = windowWidth/windowHeight;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
});
Finishing touches
While we technically have a 3D box, from this angle is just looks like a square. Nothing against squares, but why not rotate the box a little to show off its other dimensions?
We can do that by by calling the set() method on the box mesh's rotation property. It takes the x, y, and z rotations as arguments.
We only need to rotate it around one of those axes to prove its 3 dimensional, so let's rotate it, say, 4% around its y-axis (i.e. .04 x Math.PI*2, a full rotation)
While we're at it we can also double the box's scale to give us a better view. We can do that by setting scale property of each dimension to 2. The code for those box updates:
boxMesh.rotation.set(0,Math.PI*2*.04,0);
boxMesh.scale.set(2,2,2);
Adding light
Let's also take advantage of the shinyness of the box's MeshPhongMaterial. To admire it in all its glory, we can point a spotlight at it, using the ThreeJS PointLight.
We can set the light to roughly the same position as the camera (middle of the endzone, or (0,0,-5)), but offset it just slightly in the x-direction to get a more natural-looking glare on the box (e.g. (-1.5,0,-5)).
The PointLight takes a few arguments, such as color, intensity, and distance. I'll set color to white, intensity at a medium .5, and distance at 6 since we only need it to reach the 5 "yards" to the box.
const spotlight = new THREE.PointLight(0xffffff, .5,6);
spotlight.position.set(-1.5,0,-5);
scene.add(spotlight);
Updating box material
Now let's set a few parameters on the box's material to increase the shine. We can set the shininess to 50 (the default is 30 so this gives it some extra), set the color of the box to black (rgb(0,0,0)), and set the "specular", or shine color to white (rgb(255,255,255)).
The updated box material:
const boxMaterial = new THREE.MeshPhongMaterial({shininess: 50,color: new THREE.Color('rgb(0,0,0)'), specular: new THREE.Color('rgb(255,255,255)')});
Updating renderer
Finally, we can make some adjustments to the renderer to give the scene a cinematic sheen.
We can do that by setting the renderer's toneMapping property. This approximates HDR (high dynamic range) images on a LDR (low dynamic range) output device. The built-in THREE.ACESFilmicToneMapping makes the output more "filmic".
The toneMappingExposure sets the exposure level of tone mapping. The default is 1, so 1.5 makes it slightly brighter.
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.5;
Now that's what I call a box!
Considering that WebGL wasn't released until 2011, and that, according to Wikipedia, modern humans are roughly 300,000 years old, for 99.996% of human existance it wasn't possible to do what we just did—render a 3D box on a web browser using WebGL.
That's an accomplishment worthy of a monument: A black monolith.
How do you transform the box into a monolith? Simple: set the box's scale to (4*.75,9*.75,1*.75) to make it wider and taller (it must be in the ratio of 1x4x9 or 1² x 2² x 3²), rotate it slightly more around y-axis (7.5%), move the spotlight up from y=0 to y=2 and double the spotlight's intensity from .5 to 1:
const scaleRatio = .75;
boxMesh.scale.set(4*scaleRatio,9*scaleRatio,1*scaleRatio);
boxMesh.rotation.set(0,Math.PI*2*.075,0);
spotlight.position.set(-1.5,2,-5);
spotlight.intensity=1;
You can find all the code for this (and other ThreeJS examples) on Github here. If you just want the code for the simple, un-rotated box (also known as a square), that is located here.