What I Wish I’d Known About Three.js
So, you want to build the next AAA blockbuster video game title, but all you know is JavaScript? What if I told you there was a way to get all of the bells and whistles of a C++ Unreal Engine 3D masterpiece up and running in the browser without any of the big bad math that has always kept you too scared to try your hand at game design (I might be projecting a little on that last part)? You’d look at me like I was crazy (mostly because that’s a bit of an exaggeration).
Okay, so maybe you won’t be running the next Skyrim on Internet Explorer, but there is a way to bring high fidelity, realistic, 3D renderings to life in the browser. Since way back in the ancient days of 2011, the WebGL API (short for Web Graphics Library) has allowed web developers to utilize the powerful mechanics of OpenGL in order to render 2D and 3D vector graphics.
While WebGL is an incredibly useful API for graphics rendering all on its own, it can also be a handful. Take, for example, coding a simple spinning cube. It’s often refereed to as the “Hello World” of 3D rendering, and it’s the first task most people start with when learning any 3D program or API. While the cube might be the most basic step to learning 3D rendering, it actually takes quite alot of code in plain WebGL. For an example, checkout this GitHub repo by Eric Shepherd and try to mke sense of it. It takes almost 380 lines of code just to render his example shown below
Seems like an awful lot of work for one little old cube, doesn’t it? Imagine having to write out that code for every cube in your 3D world? And let’s be honest, as a web developer you probably don’t want to just render a bunch of squares over and over again. What if you want to develop more complex geometries and movements? That’s when a library like Three.js comes in handy. Three.js abstracts away all of the complex details of WebGL and lets you focus on design, animation, and interactivity.
In this series, we’re going to build a three dimensional world that a player can move around and interact with right in their web browser. But for this article, let’s focus on the very basics, the “Hello World” of 3D rendering: we’re going to make a spinning box with Three.js.
Three.js makes browser 3D rendering much simpler by narrowing down the moving parts we have to deal with. There’s a bit of setup that I’ve gone through to get a webpack server running, and for simplicity’s sake I won’t go too into depth about that. You should be able to run everything in this tutorial on Code Pen or a similar service, but if you’re interested in following along in your local dev environment, you can find my Webpack ready starting point at this GitHub repo. Feel free to use this as a general boilerplate for yourself when starting a Three.js project.
Brief side note: if you’re using the boiler plate, you’ll want to run npm install and npm start as well as open up localhost 8080 on your browser before we get started. It might also help to take a moment to familiarize yourself with the file structure. You’ll also want to navigate to main.js and write the following code: import * as THREE from “three”
. Running a Webpack server and babel lets us leverage ES6 module import syntax, which is my favorite way of bringing in Three.js modules.
Without any further ado, let’s get started with the basics. The WebGL example above took a lot of seperate pieces to get a rendering up and running. In Three.js, there are only three things you strictly need to get the same effect: Scene, Renderer, and Camera. These will be the building blocks for any work you ever do in Three.js.
The Scene is the most basic element of Three.js. It serves as the spatial refernce for our rendering. Every mesh, camera, and effect exists with a reference to some x, y, and z coordinates in the Scene. Creating a new scene is as simple as declaring const scene = new THREE.Scene()
. Moving forward, any time we want a new element to show up on our screen, we will have to declare scene.add(element)
.
The next basic building block of Three.js is the Renderer. The renderer does all of the heavy lifting of WebGL rendering. The renderer actually handles a fair chunk of what was making the native WebGL code so long. Technically there are other options besides WebGL rendering provided in Three.js, but they are generally used as fallbacks for older less advanced browsers (cough cough, Internet Explorer). A basic renderer can be created with the line const renderer = new THREE.WebGLRenderer()
. The next step is appending the renderer to the DOM. Since we have already provided a canvas element to our HTML document, we can run append the renderer.domElement (the property that allows the renderer to be added to the DOM) as a child of the HTML element with the id “canvas.” As a note, many Three.js developers chose to append the renderer directly to the HTML document body, but I like to use a devoted canvas element for ease of styling down the line.
Finally, the Camera is the frame from which everything in our scene is rendered. It’s the viewpoint from which users can see our scene, and it takes a bit more setup than the previous two steps. There are several different types of camera, the most commonly used of which are the Perspective Camera and the Orthographic Camera. The difference between these two main types is that Perspective Cameras mimic real world distance by making objects appear smaller when further away from the camera. Orthographic Cameras render an object’s size as constant, regardless of the distance from the camera. Orthographic Cameras are a great tool for design and architecture, but for the sake of this series we’ll be working exclusively with Perspective Cameras.
Once you’ve decided on the type of camera that suits your needs, you’ve got to give Three.js a bit more information on how you’d like your camera to work. The THREE.PerspectiveCamera constructor takes four parameters: Field Of View(fov), Aspect Ratio(aspect), Near Clipping Distance(near), and Far Clipping Distance(far). The fov determines how much of the scene can be displayed at once, the ratio determines the width to height comparison of the camera(often set as width/height of the screen or display element), and the clipping distances determine how near or far an object must be to the camera before the renderer stops rendering it (this is mostly a performance feature to keep the renderer from having to expend resources rendering objects outside of a given range. By default, the camera will always start at the scene origin (x=0, y=0, and z=0). Remember that, it’ll be important later!
With that in mind, we can update our code to look something like this:
//scene defines relational space that all of our 3D elements will live inconst scene = new THREE.Scene();//renderer defines the physical space on screen that will be rendering our 3D meshes and camera viewsconst renderer = new THREE.WebGL1Renderer();//REMEMBER to append renderer.domElement, not just rendererconst canvas = document.getElementById("canvas")canvas.appendChild(renderer.domElement)//camera defines the perspective from which a scene is view//we use a perspective camera here to make obejct size depend on distance//the camera constructor takes fov, aspect, near, and far propertiesconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
And Voila! You’ve got everything you need in order to start making 3D renderings. We haven’t added any objects or animation to the scene yet, but if you checkout your web page or Code Pen, you should see something like what is below:
See that little black box in the corner of the screen? That’s our renderer. Its default color is black and it’s default size is pretty small. We’ll go over different tactics for changing the background color of the renderer in a later part of this series. For now, let’s focus on fixing the size of the renderer.
The Three.js renderer has a built in property called renderer.setSize(width, height)
which allows you to set the renderer to any size you want. Let’s set the renderer to take up the entire width and height of the screen with the code: renderer.setSize(window.innerWidth, window.innerHeight)
.
If you take a look at the Code Pen above, you’ll see that the little black box has expanded to take up the full width and height of the screen. Now that we’ve got our renderer looking good, the next step is to finally make our cube.
In Three.js, 3D shapes are called meshes. They can be incredibly complex, merged, and animated to do whatever you want. For some really cool examples of the wild and imaginative things that people have been able to render with meshes, checkout the Three.js website here and have a click around at some of the examples linked on their landing page.
As complex as all of these shapes and objects might be, they are all made up of the same two building blocks of a Three.js mesh that we’re going to discuss here: Geometry and Material.
In Three.js, a geometry is the placement of vertices that will determine what a mesh’s shape will look like, and a material is a set of instructions for how that mesh is going to interact with light. There are a whole lot of predefined geometries in Three.js, and you have the ability to create custom geometries from vertices. That’s a bit beyond the scope of this first lesson, though. For now, we’re just going to make a Box Geometry. The constructor for a Box Geometry gets three parameters for width, height, and depth, like so: const geometry = new THREE.BoxGeometry(width, height, depth)
. To keep it simple, we’ll make a 1 unit cubed box (width=1, height=1, depth=1).
The second part of a mesh is material. There are several different types of material in Three.js, and we’ll be going over those options in a later lesson. For now we’ll only cover the two that take the least amount of processing power to render: Mesh Basic Material and Mesh Lambert Material. The basic material doesn’t interact with light at all and only shows its color, while the lambert material has some basic interactions with light and shadow. Mesh Lambert Material is going to look a lot more dynamic, so it’s going to be our end goal for this article. However, since we haven’t dded any lights and Mesh Basic Material is the only material that will be visible without any light source, that’s where we’ll start. All the constructor takes is a color: const material = new THREE.MeshBasicMaterial({color: colorHexadecimal})
. Something that tripped me up for a while was that when providing a hexadecimal color, Three.js expects the format of 0xColor instead of the standard #Color.
Now that we’ve got our geometry and our material, we can create our cube mesh with the constructor: const cube = new THREE.Mesh(geometry, material)
and add it to our scene with scene.add(cube)
. By default, this adds your cube mesh at the scene origin (x=0, y=0, z=0). The final step is to instruct our rederer to render the scene once with all of the information it has. This can be done by calling the render method on the renderer with the scene and camera as parameters: renderer.render(scene, camera)
.See below for what your code should look like at this point.
//npm install three and ES6 import it to get started with Three.jsimport * as THREE from 'three';//scene defines relational space that all of our 3D elements will live inconst scene = new THREE.Scene();//renderer defines the physical space on screen that will be rendering our 3D meshes and camera viewsconst renderer = new THREE.WebGLRenderer();/*ADD LINE BELOW to make renderer take up the full screen*/renderer.setSize(window.innerWidth, window.innerHeight)//REMEMBER to append renderer.domElement, not just rendererconst canvas = document.getElementById("canvas")canvas.appendChild(renderer.domElement)//camera defines the perspective from which a scene is view//we use a perspective camera here to make obejct size depend on distance//the camera constructor takes fov, aspect, near, and far propertiesconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)//OBJECTS SECTION//meshes are made up of a geometry and a materialconst geometry = new THREE.BoxGeometry(1,1,1)const material = new THREE.MeshBasicMaterial({color: 0x0000ff})const cube = new THREE.Mesh(geometry, material)//add cube to scenescene.add(cube)renderer.render(scene, camera)
If you’re following along, you’ve probably realized that your computer screen is still an empty black box. That’s okay. Nothing has gone wrong, we’ve just missed one important step. Both the camera and the mesh are being rendered at the scene origin! Let’s fix that with the camera’s built in property of position. All meshes and cameras in Three.js have a position property, with an x, y and z value to tell us where they are positioned in comparison to the scene origin. We can either set x, y, and z position coordinates individually by writing something along the lines of element.position.x = x
, or all at once with the method element.position.set(x, y, z)
. We can also assign our camera to always look at our cube with camers.lookAt(cube.position)
. The lookAt method can be given a coordinate position, or the position of a mesh.
It’s important that you add the camera position logic ABOVE the renderer.render(scene, camera)
call. The render method only renders once with all of the information above the call. We’ll get to animation in a couple of steps. But for now, you should have something like this rendering on your screen:
Not bad, but not great either. There’s no definition to the edges of our cube, so it looks a bit like a weird bottom-heavy hexagon. That’s because we haven’t added light to the scene to create shadows and define our shape. The next article in this series will discuss lighting more in depth, because it’s a pretty big topic. In this lesson, we’ll just be adding a simple directional light to the scene.
Three.js can replicate a whole slew of different kinds of lights, but Directional Light is one of the most simple and common kinds. All that a Directional Light does is give an even wash of light to the scene from one direction. It replicates a very large light source like the sun, where the effect of the light is the same on every object in the scene. To create a directional light, the only required parameter is light color, though you can optionally define a light intensity. The constructor is as follows: const directionalLight = new THREE.DirectionalLight(color, intensity)
. By default a new directional light will be pointing down the z axis and its target will be the scene origin. Unless we manually change the target, it will always point at the origin. Above our render call, let’s create a new directional light, add it to our scene, and use position.set()
to change the direction it’s coming from to something that will light up our object a little more unevenly.
If you’ve been following me up until now, you should have a cube with nicely defined edges and faces, and your code should look like this:
//scene defines relational space that all of our 3D elements will live inconst scene = new THREE.Scene();//renderer defines the physical space on screen that will be rendering our 3D meshes and camera viewsconst renderer = new THREE.WebGLRenderer();/*ADD LINE BELOW to make renderer take up the full screen*/renderer.setSize(window.innerWidth, window.innerHeight)//REMEMBER to append renderer.domElement, not just rendererconst canvas = document.getElementById("canvas")canvas.appendChild(renderer.domElement)//camera defines the perspective from which a scene is view//we use a perspective camera here to make obejct size depend on distance//the camera constructor takes fov, aspect, near, and far propertiesconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)//OBJECTS SECTION//meshes are made up of a geometry and a materialconst geometry = new THREE.BoxGeometry(1,1,1)const material = new THREE.MeshLambertMaterial({color: 0x0000ff})const cube = new THREE.Mesh(geometry, material)//add cube to scenescene.add(cube)// change camera position so that we can see the cubecamera.position.set(1,1,1)// direct camera at the cube positioncamera.lookAt(cube.position)// create directional lightconst directionalLight = new THREE.DirectionalLight(0xffffff)scene.add(directionalLight)// change light positiondirectionalLight.position.set(3,2,1)// single render callrenderer.render(scene, camera)
That’s looking a lot better! Now we take a plunge into the final frontier: animation. You’re probably not happy just rendering a motionless box on the screen. You want some way to move your renderings around. How else are you going to code the next Grand Theft Auto in Internet Explorer (again, maybe a bit of an exaggeration)? That’s where the animation function comes into play.
Technically, this could be named anything, but I tend to call it animate because that’s what it’s doing. The real magic comes from the requestAnimationFrame()
function. The idea behind the animate function is that we want to render our scene not jut once, but once every time our screen refreshes. Within the function, we need to call renderer.render()
, create any animation update logic, and use requestAnimationFrame(animate)
to recursively call the whole animate function in an endless loop. A basic animate function might look like this:
function animate(){renderer.render(scene, camera)// INSERT ANIMATION UPDATE LOGIC HERErequestAnimationFrame(animate)}
We’ve already discussed that most elements in a Three.js scene have a position property with x, y, and z values, but the same is true for rotation. We can rotate objects around any axis with element.rotation.x = x
or element.rotation.set(x, y, z)
. Write an animate function that will change the x and y rotation of your cube by a set amount each render cycle. And always remember to call animate()
at the very end of your file.
All in all, your file should look like this:
//scene defines relational space that all of our 3D elements will live inconst scene = new THREE.Scene();//renderer defines the physical space on screen that will be rendering our 3D meshes and camera viewsconst renderer = new THREE.WebGLRenderer();/*ADD LINE BELOW to make renderer take up the full screen*/renderer.setSize(window.innerWidth, window.innerHeight)//REMEMBER to append renderer.domElement, not just rendererconst canvas = document.getElementById("canvas")canvas.appendChild(renderer.domElement)//camera defines the perspective from which a scene is view//we use a perspective camera here to make obejct size depend on distance//the camera constructor takes fov, aspect, near, and far propertiesconst camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)//OBJECTS SECTION//meshes are made up of a geometry and a materialconst geometry = new THREE.BoxGeometry(1,1,1)const material = new THREE.MeshLambertMaterial({color: 0x0000ff})const cube = new THREE.Mesh(geometry, material)//add cube to scenescene.add(cube)// change camera position so that we can see the cubecamera.position.set(1,1,1)// direct camera at the cube positioncamera.lookAt(cube.position)// create directional lightconst directionalLight = new THREE.DirectionalLight(0xfffff)scene.add(directionalLight)// change light positiondirectionalLight.position.set(3,2,1)// animation cycle will recursively call requestAnimationFrame and animate your scenefunction animate(){renderer.render(scene, camera)cube.rotation.x += 0.01cube.rotation.y += 0.01;requestAnimationFrame(animate)}animate()
And your browser should be displaying a fully animated cube!
Congratulations! You’ve taken your first step into 3D rendering! Next stop, E3 and game of the year! You can check out the finished Webpack ready version of this tutorial here on GitHub. I hope you’ve enjoyed this lesson on the basics of Three.js, and I hope you’ll be back for part two where we’ll be experimenting a bit more with lighting!