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.
|
|
- 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 theWebGLRenderer
is the most complete and the one we will be using. This is similar to passingP2D
orWEBGL
in thecreateCanvas()
function of p5.js. - Setting
antialias
makes the edges smoother in the render.
|
|
- The
scene
is like a stage, which will hold everything that will be rendered by therenderer
. We will add our meshes to the scene to draw them. - Setting the background color is equivalent to the p5.js
background()
function.
|
|
- 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.
|
|
- 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 usingrequestAnimationFrame()
.
|
|
- The
resize
callback is similar to thewindowResized()
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.
In the next class, we will learn how to use these attributes in our own custom three.js shaders.