Intro to three.js

We have been using p5.js exclusively up until now, but it’s time to try something new. p5.js is great for sketching ideas quickly and for working in 2D in general, but it’s a little lacking when working in 3D. It also has its own approach to WebGL and rendering graphics, which is heavily based on its predecessor Processing.

We will now look at a different framework called three.js. It has some pros and cons compared to p5.js and being familiar with both will be useful depending on the type of project we want to take on.

Three.js Boilerplate

three.js is a very popular JavaScript framework for rendering 3D graphics. It also uses WebGL under the hood but has a very different approach from p5.js with how an app is organized. The programmer is responsible for adding and maintaining everything in the sketch, from the renderer to the update loop. While this may seem daunting at first, it is basically a lot of boilerplate code we just need to copy-paste add at the beginning of each sketch to get going. The advantage with this approach is that we will gain a better understanding of how things work under the hood, and will have more control over any parameters we are using.

Let’s take a look at a barebones three.js sketch and go through each section. A lot of this will look familiar if you have experience with game engines.

1
2
3
const canvas = document.querySelector("#canvas");
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
  • The canvas is the HTML DOM element that three.js will draw into.
  • The renderer is the engine used to render graphics. three.js includes a few rendering engines, but the WebGLRenderer is the most complete and the one we will be using. This is similar to passing P2D or WEBGL in the createCanvas() function of p5.js.
  • Setting antialias makes the edges smoother in the render.
1
2
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x292f33);
  • The scene is like a stage, which will hold everything that will be rendered by the renderer. We will add our meshes to the scene to draw them.
  • Setting the background color is equivalent to the p5.js background() function.
1
2
3
4
5
6
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight
);
camera.position.z = -3;
scene.add(camera);
  • The camera is our view into the scene. The camera has a position, an orientation, a field of view, and an aspect ratio, which all determine which parts of the scene can be seen and which are hidden.
  • p5.js has a default camera that looks directly at the canvas, but it also has a customizable p5.Camera with similar parameters.
1
2
3
4
5
6
const tick = () => {
  renderer.render(scene, camera);

  requestAnimationFrame(tick);
};
tick();
  • The animation loop in three.js must be explicitly set. This is where objects can be moved, parameters can be changed, etc. This is similar to update() in p5.js.
  • In its most basic version, the animation function should render the scene using renderer.render() and should request to be called again using requestAnimationFrame().
1
2
3
4
5
6
7
window.addEventListener("resize", () => {
  const w = window.innerWidth;
  const h = window.innerHeight;
  camera.aspect = w / h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
});
  • The resize callback is similar to the windowResized() function in p5.js.
  • In its most basic version, it should update the renderer size and the camera aspect ratio to ensure that our graphics are at the correct scale and aren’t stretched.

Three.js Meshes

Anything that is drawn to the screen is a Mesh. A mesh needs two components: a BufferGeometry and a Material.

We can think of a BufferGeometry as a set of vertices, and a Material as a shader. Like in p5.js, we don’t need to always explicitly define our vertices and shaders. three.js has some built-in objects to make our life easier.

Let’s draw a box (cube) to the screen. We will first create a BoxGeometry and a MeshBasicMaterial. We will then use those to create a mesh, and finally add the mesh to the scene.

The box is drawn at the origin (0, 0, 0) by default. We need to tell the camera to look at the origin for the box to be visible.

The box position can be set by changing the mesh’s position property. This is a 3D vector with components xyz. If we play with these values, we notice that the axes are a bit different than with p5.js.

  • X moves from right to left.
  • Y moves from bottom to top.
  • Z moves from front to back.

The box orientation can be set by changing the mesh’s rotation property. The rotation is a 3D vector with components xyz.

This may look similar to the transformations translate() and rotateX/Y/Z() in p5.js but there is an important difference. In three.js, we are transforming the object itself and not the scene. We don’t need to push and pop or revert the transform when we draw the next object, each one has its own transformation matrix (model matrix).

Objects in three.js can be organized in a parent-child relationship. Instead of adding objects to the scene, we can add them to other objects already in the scene. These objects can be other meshes or they can be a Group, which is like an empy object that has a transformation, but does not get drawn.

Children of objects inherit their parent’s transformation. In the example above, both the box and the sphere are rotating because we are rotating their parent group. This type of nesting of objects can be very useful when building complex scenes.

Let’s add some interactivity to our scene. We will use OrbitControls to control the camera using the mouse. This object is not part of the standard three.js library, so we will need an additional import at the top of the file.

If we examine the geometry objects we have created, we will see they include an attributes object. The attributes holds arrays named position, color, uv, and normal. Some of these should look familiar! Indeed, these are the vertex attributes for this mesh. We already know what position and color are; uv is another name for texture coordinates; and normal is a xyz vector that represents the direction the point is facing.

Attributes

In the next class, we will learn how to use these attributes in our own custom three.js shaders.