Transformations, Coordinate Systems, and the Scene Graph
This chapter is an introduction to moving objects around in 3D space.
Many things come together to make a beautiful 3D scene, such as lighting, materials, models, textures, camera settings, post-processing, particle effects, interactivity, and so on, but no matter what kind of scene we create, nothing is more important than the arrangement and movement of the pieces from which it is composed.
To create architectural renderings, we must become architects and interior decorators. We must consider the proportions of buildings and the rooms inside them, and skillfully place the furniture and light fittings. In a nature scene, whether a close up of a single flower or a wide, sweeping mountain vista, we need to arrange the trees and rocks, or the leaves and petals, in a natural and convincing manner. Perhaps a hoard of invading robots will sweep across the landscape, eyes gleaming, arms and feet swinging as they march in unison, rockets blasting into the sky and creating huge explosions wherever they land - in which case we must become both robot designers and ballistics experts.
Even purely abstract scenes require an understanding of how to move objects around in 3D space.
Finally, we must also become directors and position the camera to artistically frame each shot. When creating a 3D scene, the only limit is your imagination - and the depth of your technical knowledge.
Moving objects around in 3D space is a fundamental skill on your path of learning three.js. We’ll break this skill down into two parts: first, we’ll explore the coordinate system used to describe 3D space, and then we’ll explore mathematical operations called transformations that are used to move objects around within a coordinate system.
Along the way, we’ll encounter several mathematical objects, such as the scene graph, a structure used to describe the hierarchy of objects that make up our scenes, vectors, which are used to describe positions in 3D space (and many other things), and no less than two ways of describing rotations: Euler angles and quaternions. We’ll finish up the chapter by introducing you to transformation matrices, which are used to store an object’s complete transformation state.
Translation, Rotation, and Scaling: the Three Fundamental Transformations
Whenever we move objects around in 3D space, we do so using mathematical operations called transformations. We’ve already seen two kinds of transformation: translation, stored in an object’s
.position
property, and rotation, stored in the
.rotation
property. Along with scaling, stored in the
.scale
property, these make up the three fundamental transformations that we’ll use to move objects around in our scenes. We’ll sometimes refer to transform, rotate, and scale using their initials, TRS.
Every object we can add to the scene using scene.add
has these properties, including meshes, lights, and cameras, while materials and geometries do not. We previously used .position
to
set the position of our camera:
… and also to
set the position of the directional light:
set the position of the directional light
In the last chapter
we used .rotation
to get a better view of our cube:
The only fundamental transformation we haven’t encountered so far is .scale
.
There’s no code to write in this chapter. Instead, the editor has a mesh set up with some transformations applied, which you can use as a scratchpad for testing out ideas while you read.
Butterflies and Caterpillars
Using the word transformation in this way might seem strange to you. In common speech, it’s more likely to evoke the idea of a caterpillar turning into a butterfly than moving the caterpillar two units to the left across a leaf. But mathematically speaking, only the second one of these is a transformation. Translation, rotation, and scaling are the most important transformations you’ll encounter, and we’ll explore each of these in detail in a few moments.
The Object3D
Base Class
Rather than redefining the .position
, .rotation
, and .scale
properties many times for each type of object, these properties are defined once on the
Object3D
base class, then all the other classes that can be added to the scene
derive from this base class. That includes things like meshes, cameras, lights, points, lines, helpers, and even the scene itself. We’ll informally refer to classes derived from Object3D
as scene objects.
Object3D
has many properties and methods besides these three, inherited by every scene object. This means positioning and setting up a camera or a mesh works in much the same way as setting up a light or the scene. Additional properties are then added to scene objects as needed, so lights get color and intensity settings, the scene gets a background color, meshes get a material and geometry, and so on.
The Scene Graph
Recall how we add the mesh to scene:
The .add
method is also defined on Object3D
and inherited by the scene class, just like .position
, .rotation
, and .scale
. All other derived classes inherit this method too, giving us light.add
, mesh.add
, camera.add
and so on. This means we can add objects to each other to create a tree structure with the scene at the top. This tree structure is known as the scene graph.
When we add an object to another object, we call one object the parent and the other the child.
The scene is the top-level parent. The scene in the figure above has three children: one light and two meshes. One of the meshes also has two children. However, every object (except the top-level scene) has exactly one parent.
Each object in the scene graph (except the top-level scene) has exactly one parent, and can have any number of children.
When we render the scene:
… the renderer walks through the scene graph, starting with the scene, and uses the position, rotation, and scale of each object relative to its parent to figure out where to draw it.
Accessing a Scene Object’s Children
You can access all children of a scene object using the
.children
array:
There are more sophisticated ways to access a particular child, for example, the
Object3d.getObjectByName
method. However, directly accessing the .children
array is useful when you don’t know the object’s name, or it doesn’t have a name.
Coordinate Systems: World Space and Local Space
3D space is described using a 3D Cartesian coordinate system.
3D Cartesian coordinate systems are represented using $X$, $Y$, and $Z$ axes crossing at the point $(0,0,0)$ (known as the origin). 2D coordinate systems are similar but have only $X$, and $Y$ axes.
Every 3D graphics system uses a coordinate system like this, from game engines like Unity and Unreal, to the software Pixar uses to create their films, to professional animation and modeling software such as 3DS Max, Maya, and Blender. Even CSS, the language used to position objects on a web page, uses a Cartesian coordinate system. However, there may be minor technical differences between these systems, such as the axes being labeled differently or pointing in different directions.
We’ll encounter several 2D and 3D coordinate systems while using three.js. Here, we’ll introduce the two most important of these: world space and local space.
World Space
Our scene
defines the world space coordinate system, and the center of the system is the point where the X
, Y
and, Z
axes meet.
Remember a couple of chapters ago,
when we first introduced the Scene
class, we called it a “tiny universe”? This tiny universe is world space.
When we arrange objects within a scene - whether we are positioning furniture in a room, trees in a forest, or rampaging robots on a battlefield - what we see drawn on our screens is the position of each object in world space.
When we add an object directly to the scene and then translate, rotate, or scale it, the object will move relative to world space - that is, relative to the center of the scene.
These two statements are equivalent, so long as the object is a direct child of the scene:
- Transform an object relative to world space.
- Move an object around in the scene.
Whenever we try to visualize something tricky in 3D, it can be useful to drop down a dimension and consider a 2D analogy instead. So, let’s consider a chessboard. When we arrange the pieces to start a new game, we place them in certain positions on the board. That means the chessboard is the scene, and the pieces are objects we place in the scene.
Next, when we explain to someone why we have arranged the pieces like this, white on one side, black on the other, pawns on the second row, and so on, we do so relative to the board itself. The board defines a coordinate system, with rows on the Y-axis and columns on the X-axis. This is the world space of a chessboard, and we explain the position of each piece relative to this coordinate system.
Now the game starts, and we begin to move pieces. When we do, we follow the rules of chess. When we move an object around in a three.js scene, we follow the rules of Cartesian coordinate systems. Here the analogy breaks down a little because each piece on the chessboard has its own way of moving, whereas in a Cartesian coordinate system, translation, rotation, and scale behave the same for any kind of object.
Local Space
Now, consider one of the chess pieces. If asked to describe the shape of a chess piece, you won’t describe how it looks relative to the chessboard since it may be placed anywhere on the board, and indeed, retains its shape even when not on the board at all. Instead, you’ll create a new coordinate system in your mind and describe how the piece looks there.
Just like pieces on a chessboard, every object we can add to the scene also has a local coordinate system, and the shape (geometry) of the object is described within this local coordinate system. When we create a mesh or a light, we also create a new local coordinate system, with the mesh or light at its center. This local coordinate system has $X$, $Y$ and, $Z$ axes, just like world space. The local coordinate system of an object is called local space (or sometimes object space).
When we create a $2 \times 2 \times 2$ BoxBufferGeometry
, and then create a mesh using the geometry, the size of the geometry is two units along each side in the mesh’s local space:
As we’ll see below, we can stretch or shrink the mesh using .scale
, and the size of the mesh as drawn on our screen will change. However, the size of the geometry does not change when we scale the mesh. When the renderer comes to render the mesh, it will see that it has been scaled, and then draws the mesh at a different size.
Every Object has a Coordinate System
To recap: the top-level scene defines world space, and every other object defines its own local space.
With the above three lines of code, we have created three coordinate systems. There’s no difference, mathematically, between these three coordinate systems. Any mathematical operation we can do in world space will work the same way in any object’s local space.
It’s easy to think of coordinate systems as big complicated things, however, when working in 3D you’ll find out that there are a lot of coordinate systems around. Every object has at least one, and some have several. There’s another whole set of coordinate systems involved in rendering the scene, that is, converting the objects from 3D world space into something that looks good on the flat 2D surface of your screen. Every texture even has a 2D coordinate system. In the end, they are not so complicated, and they are very cheap to create.
Working with the Scene Graph
Using each object’s .add
and .remove
methods, we can create and manipulate the scene graph.
When we add an object to our scene using scene.add
, we embed this object within the scene’s coordinate system, world space. When we move the object around, it will move relative to world space (or equivalently, relative to the scene).
When we add an object to another object deeper within the scene graph, we embed the child object within the parent’s local space. When we move the child object around, it will move relative to the parent object’s coordinate system. The coordinate systems get nested inside each other like Russian dolls.
Let’s look at some code. First, we’ll add an object $A$ as a child of the scene:
Now, the scene
is the parent of $A$, or equivalently, $A$ is a child of the scene
. Next, we’ll translate $A$:
Now, $A$ has been translated five units along the positive $X$-axis within world space. Whenever we transform an object, we do so relative to its parent’s coordinate system. Next, let’s look at what happens when we add a second object, $B$, as a child of $A$:
$A$ is still a child of the scene, so we have the relationship $Scene \longrightarrow A \longrightarrow B$. So, $A$ is a child of the scene and $B$ is a child of $A$. Or, equivalently, $A$ now lives in world space and $B$ now lives in $A$’s local space. When we move $A$, it will move around in world space, and when we move $B$, it will move around in $A$’s local space.
Next, we’ll translate $B$:
Where do you think $B$ will end up?
What We See is World Space
When we call .render
, the renderer calculates the world space position of each object. To do this, it starts at the bottom of the scene graph and works its way up, combining the transformations of each parent and child, to calculate the final position of each object relative to world space. What we finally see on our screen is world space. Here, we’ll do this calculation by hand for $A$ and $B$. Remember, each object starts at $(0,0,0)$ relative to its parent.
Calculating $A$’s position is easy since it’s a direct child of the scene. We moved $A$ five units to the right along the $X$-axis, so its final position is $x=5, y=0, z = 0$, or $(5, 0, 0)$.
When we move $A$, its local coordinate system moves with it, and we must take that into account when calculating the world space position of $B$. Since, $B$ is a child of $A$, this means it now starts at $(5, 0, 0)$ relative to world space. Next, we moved $B$ three units along the $X$-axis relative to $A$, so the final position of $B$ on the $X$-axis is $5 + 3 = 8$. This gives us the final position of $B$ in world space: $(8, 0, 0)$.
Moving an Object Between Coordinate Systems
What happens if we move an object from one coordinate system to another? In other words, what happens if we take mesh $B$, and, without changing its .position
, remove it from $A$ and add it directly to the scene? We can do this in a single line:
An object can only have one parent, so any previous parent of $B$ (in this case, mesh $A$) is removed.
The following statement still holds: $B$ has been translated three units along the positive $X$-axis within its parent’s coordinate system. However, $B$’s parent is now the scene rather than $A$, so now we must recalculate the position of $B$ in world space rather than $A$’s local space, which will give us $(3, 0, 0)$.
That’s it for coordinate systems. In the rest of the chapter, we’ll take a deeper look at each of the three fundamental transformations: translation, rotation, and scale.
Our First Transformation: Translation
The simplest of the three fundamental transformations is translation. We’ve already used it for several of the examples in this chapter, and also to set the position of the camera and light in our scene. We perform translations by changing an object’s
.position
property. Translating an object moves it to a new position within the coordinate system of its direct parent.
To fully describe an object’s position, we need to store three pieces of information:
- The object’s position on the $X$-axis, which we call $x$.
- The object’s position on the $Y$-axis, which we call $y$.
- The object’s position on the $Z$-axis, which we call $z$.
We can write these three positions as an ordered list of numbers: $(x, y, z)$.
Zero on all three axes is written $(0,0,0)$, and as we mentioned previously, this point is known as the origin. Every object starts at the origin within the coordinate system of its parent.
A position one unit to the right along the $X$-axis, two units up along the $Y$-axis, and three units out along the $Z$-axis is written $(1,2,3)$. A position two units left along the $X$-axis, four units down along the $Y$-axis, and eight units in along the $Z$-axis is written $(-2,-4,-8)$.
We call an ordered list of numbers like this a vector, and since there are three numbers, it’s a 3D vector.
Translating an Object
We can translate along the $X$, $Y$, and $Z$ axes one by one, or we can translate along all three axes at once using position.set
. The final result in both cases will be the same.
When we perform the translation $(1,2,3)$, we are performing the mathematical operation:
$$(0,0,0) \longrightarrow (1,2,3)$$
This means: move from the point $(0,0,0)$ to the point $(1,2,3)$.
The Unit of Translation is Meters
When we perform the translation mesh.position.x = 2
, we move the object two three.js units to the right along the $X$-axis, and
as we mentioned previously, we’ll always take one three.js unit to be equal to one meter.
Directions in World Space
Above we mentioned moving an object left or right on the $X$-axis, up or down on the $Y$-axis, and in or out on the $Z$-axis. These directions are relative to your screen and assume that you have not rotated the camera. In that case, the following directions hold:
 
- The positive $X$-axis points to the right of your screen.
- The positive $Y$-axis points up, towards the top of your screen.
- The positive $Z$-axis points out of the screen towards you.
Then, when you move an object:
- A positive translation on the $X$-axis moves the object to the right on your screen.
- A positive translation on the $Y$-axis moves the object up towards the top of your screen.
- A positive translation on the $Z$-axis moves the object out towards you.
When we put a minus sign into the translation, we reverse those directions:
- A negative translation on the $X$-axis moves the object to the left on your screen.
- A negative translation on the $Y$-axis moves the object down towards the bottom of your screen.
- A negative translation on the $Z$-axis moves the object in, away from you.
But of course, you can rotate the camera in any direction, in which case these directions will no longer hold. After all, what you see on your screen is the viewpoint of the camera. However, it’s useful to be able to describe directions in world space using “normal” language, so we’ll treat this camera position as the default view and continue to describe directions using this terminology, no matter where the camera happens to be.
Positions are stored in the Vector3
Class
Three.js has a special class for representing 3D vectors called
Vector3
. This class has .x
, .y
and .z
properties and methods like .set
to help us manipulate them. Whenever we create any scene object, such as a Mesh
, a Vector3
is created automatically and stored in .position
:
We can also create Vector3
instances ourselves:
We can access and update the .x
, .y
and .z
properties directly, or we can use .set
to change all three at once:
As with nearly all three.js classes, we can omit the parameters to use default values. If we omit all three parameters the Vector3
created will represent the origin, with all zero values:
three.js also has classes representing 2D vectors and 4D vectors, however, 3D vectors are by far the most common type of vector we’ll encounter.
Vectors are General Purpose Mathematical Objects
Vectors can represent all kinds of things, not just translations. Any data that can be represented as an ordered list of two, three, or four numbers are usually stored in one of the vector classes. These data types fall into three categories:
- A point in space.
- A length and direction within a coordinate system.
- A list of numbers with no deeper mathematical meaning.
Category two is the mathematical definition of a vector, and translation falls into this category. Categories one and three are not technically vectors. However, it’s useful to reuse the code within the vector classes so we’ll turn a blind eye to this.
Our Second Transformation: Scaling
Scaling an object makes it larger or smaller, so long as we scale by the same amount on all three axes. If we scale the axes by different amounts, the object will become squashed or stretched. As a result, scaling is the only one of the three fundamental transformations that can change the shape of an object.
Like .position
, .scale
is stored in a Vector3
, and the initial scale of an object is $(1,1,1)$:
Scale Values are Relative to the Initial Size of the Object
Since .scale
and .position
are both stored in a Vector3
, scaling an object works much the same way as translating it. However, while translation uses three.js units, scale does not use any units. Instead, scale values are proportional to the initial size of the object: 1 means 100% of initial size, 2 means 200% of initial size, 0.5 means 50% of initial size, and so on.
Uniform Scaling: Use the Same Value for all Three Axes
When we scale all three axes by the same amount, the object will expand or shrink, but maintain its proportions. This is called uniform scaling. A scale of $(1,1,1)$, meaning 100% scale on the $X$-axis, $Y$-axis, and $Z$-axis, is the default value:
A scale of $(2,2,2)$ means 200% scale on the $X$-axis, $Y$-axis, and $Z$-axis. The object will grow to twice its initial size:
A scale of $(0.5,0.5,0.5)$ means 50% scale on the $X$-axis, $Y$-axis, and $Z$-axis. The object will shrink to half its initial size:
Non-Uniform Scaling: Different Scale Values on Each Axis
If we scale individual axes the object will lose its proportions and become squashed or stretched. This is called non-uniform scaling. If we scale just the $X$-axis, the object will become wider or narrower:
Scaling on the $Y$-axis will make the object taller or shorter:
Finally, if we scale on the $Z$-axis, the depth of the object will be affected:
Once again, we can use .set
to scale on all three axes at once:
Negative Scale Values Mirror an Object
Scale values less than zero will mirror the object in addition to making it smaller or larger. A scale value of $-1$ on any single axis will mirror the object without affecting the size:
Values less than zero and greater than $-1$ will mirror and squash the object:
Values less than $-1$ will mirror and stretch the object:
Uniform Scale and Mirror
To mirror an object while maintaining its proportions, use the same value for all three axes but make one of them negative. For example, to double an object’s size and mirror on the $Y$-axis, use a scale value of $(2, -2, 2)$:
Or, to shrink the object to one-tenth size and mirror on the $X$-axis, use a scale value of $(-0.1,0.1,0.1)$:
Cameras and Lights Cannot be Scaled
Not all objects can be scaled. For example, cameras and lights (except for RectAreaLight
) don’t have a size, so scaling them doesn’t make sense. Changing camera.scale
or light.scale
will have no effect.
Our Final Transformation: Rotation
Rotation requires a little more care than translation or scaling. There are several reasons for this, but the main one is the order of rotation matters. If we translate or scale an object on the $X$-axis, $Y$-axis, and $Z$-axis, it doesn’t matter which axis goes first. These three translations give the same result:
- Translate along $X$-axis, then along the $Y$-axis, then along the $Z$-axis.
- Translate along $Y$-axis, then along the $X$-axis, then along the $Z$-axis.
- Translate along $Z$-axis, then along the $X$-axis, then along the $Y$-axis.
These three scale operations give the same result:
- Scale along $X$-axis, then along the $Y$-axis, then along the $Z$-axis.
- Scale along $Y$-axis, then along the $X$-axis, then along the $Z$-axis.
- Scale along $Z$-axis, then along the $X$-axis, then along the $Y$-axis.
However, these three rotations may not give the same result:
- Rotate around $X$-axis, then around the $Y$-axis, then around the $Z$-axis.
- Rotate around $Y$-axis, then around the $X$-axis, then around the $Z$-axis.
- Rotate around $Z$-axis, then around the $X$-axis, then around the $Y$-axis.
As a result, the humble Vector3
class that we used for both .position
and .scale
is not sufficient for storing rotation data. Instead, three.js has not one, but two mathematical classes for storing rotation data. We’ll look at the simpler of these here:
Euler angles. Fortunately, it’s similar to the Vector3
class.
Representing Rotations: the Euler
class
Euler angles are represented in three.js using the
Euler
class. As with .position
and .scale
, an Euler
instance is automatically created and given default values when we create a new scene object.
As with Vector3
, there are .x
, .y
and .z
properties and a .set
method:
Once again, we can create Euler
instances ourselves:
Also like Vector3
, we can omit the parameters to use default values, and again, the default is zero on all axes:
Euler Rotation Order
By default, three.js will perform rotations around the $X$-axis, then around the $Y$-axis, and finally around the $Z$-axis, in an object’s local space. We can change this using the
Euler.order
property. The default order is called ‘XYZ’, but ‘YZX’, ‘ZXY’, ‘XZY’, ‘YXZ’ and ‘ZYX’ are also possible.
We won’t get into rotation order further here. Usually, the only time you need to change the order is when dealing with rotation data from another app. Even then, this is usually taken care of by the three.js loaders. For now, if you like, you can simply think of Euler
as a Vector3
. Until you start to create animations or perform complex mathematical operations involving rotations, it’s unlikely you’ll run into any problems by doing so.
The Unit of Rotation is Radians
You may be familiar with expressing rotations using degrees. There are $360^{\circ}$ in a circle, $90^{\circ}$ in a right-angle, and so on. The perspective camera’s field of view, which we encountered earlier, is specified in degrees.
However, all other angles in three.js are specified using
radians rather than degrees. Instead of $360^{\circ}$ in a circle, there are $2\pi$ radians. Instead of $90^{\circ}$ in a right-angle, there are $\frac{\pi}{2}$ radians. If you’re comfortable using radians, great! As for the rest of us, we can use the
.degToRad
utility to convert from degrees to radians.
Here, we can see that $90^{\circ}$ is equal to $1.57079…$, or $\frac{\pi}{2}$ radians.
The Other Rotation Class: Quaternions
We mentioned above that three.js has two classes for representing rotations. The second, which we’ll mention only in passing here, is the
Quaternion
class. Along with the Euler
, a Quaternion
is created for us and stored in the .quaternion
property whenever we create a new scene object such as a mesh:
We can use quaternions and Euler angles interchangeably. When we change mesh.rotation
, the mesh.quaternion
property is automatically updated, and vice-versa. This means we can use Euler angles when it suits us, and switch to quaternions when it suits us.
Euler angles have a couple of shortcomings that become apparent when creating animations or doing math involving rotations. In particular, we cannot add two Euler angles together (more famously, they also suffer from something called
gimbal lock). Quaternions don’t have these shortcomings. On the other hand, they are harder to use than Euler angles, so for now we’ll stick with the simpler Euler
class.
For now, make a note of these two ways to rotate an object:
- Using Euler angles, represented using the
Euler
class and stored in the.rotation
property. - Using quaternions, represented using the
Quaternion
class and stored in the.quaternion
property.
Important Things to Know About Rotating Objects
Despite the issues we highlighted in this section, rotating object is generally intuitive. Here are a couple of important things to take note of:
- Not all objects can be rotated. For example,
the
DirectionalLight
we introduced in the last chapter cannot be rotated. The light shines from a position, towards a target, and the angle of the light is calculated from the target’s position, not the.rotation
property. - Angles in three.js are specified using radians, not degrees. The only exception is the
PerspectiveCamera.fov
property which uses degrees to match real-world photography conventions.
Transformation Matrices
We’ve covered a lot of ground in this chapter. We’ve introduced Cartesian coordinate systems, world space and local space, the scene graph, translations, rotations, and scaling and the associated .position
, .rotation
, and .scale
properties, and three mathematical classes used for storing transformations: Vector3
, Euler
, and Quaternion
. Surely we couldn’t cram anything else in?
Well, just one more thing. We can’t end a chapter on transformations without discussing transformation matrices. While vectors and Euler angles are (relatively) easy for us humans to work with, they are not efficient for computers to process. As we chase the elusive goal of sixty frames per second, we must walk a fine line between ease of use and efficiency. To this end, the translation, rotation, and scale of an object are combined into a single mathematical object called a matrix. Here’s what the matrix for an object that has not been transformed looks like.
It has four rows and four columns, so it’s a $4 \times 4$ matrix, and it’s storing an object’s complete transform which is why we refer to it as a transformation matrix. Once again, there is a three.js class to handle this type of mathematical object, called
Matrix4
. There’s also a class for $3\times3$ matrices called Matrix3
. When the matrix has all ones on the
main diagonal and zeros everywhere else like the one above, we call it the
identity matrix, $I$.
Matrices are much more efficient for your CPU and GPU to work with than the individual transforms, and represents a compromise that gives us the best of both worlds. We humans can use the simpler .position
, .rotation
, and .scale
, properties, then, whenever we call .render
, the renderer will update each object’s matrices and use them for internal calculations.
We’ll spend a bit of time here going into how transformation matrices work, but if you’re allergic to math, it’s absolutely fine to skip this section (for now). You don’t need a deep understanding of how matrices work to use three.js. You can stick with using .position
, .rotation
, and .scale
and let three.js handle the matrices. On the other hand, if you’re a mathematical wizard, working directly with the transformation matrix opens up a whole new range of opportunities.
The Local Matrix
Every object has, in fact, not one, but two transformation matrices. The first of these is the local matrix, which holds the combined .position
, .rotation
, and .scale
of an object. The local matrix is stored in the
Object3D.matrix
property. Every object that inherits from Object3D
has this property.
// when we create a mesh
const mesh = new Mesh();
// ... internally, three.js creates a Matrix4 for us:
mesh.matrix = new Matrix4();
At this point, the matrix will look like the identity matrix above, with ones on the main diagonal and zeros everywhere else. If we change the position of the object, and then force the matrix to update:
mesh.position.x = 5;
mesh.updateMatrix();
… now, the local matrix of the mesh will look like this:
Normally, we don’t need to call .updateMatrix
manually, since the renderer will update the matrix of every object before it’s rendered. Here, though, we want to see the change in the matrix immediately so we must force an update.
If we change the position on all three axes and update the matrix again:
mesh.position.x = 2;
mesh.position.y = 4;
mesh.position.z = 6;
mesh.updateMatrix();
… now we can see that translations are stored in the first three rows of the last column of the matrix.
Next, let’s do the same for scale:
mesh.scale.x = 5;
mesh.scale.y = 7;
mesh.scale.z = 9;
mesh.updateMatrix();
… and we’ll see that the scale values are stored on the main diagonal.
Great! That means we can write a formula for storing translation and scale in a transformation matrix. If we write the translation values as $T_{x}, T_{y}, T_{z}$, and the scale values as $S_{x}, S_{y}, S_{z}$:
… now the transformation matrix looks like this:
Finally, let’s see how rotation is stored. First, let’s reset the position and scale:
Now the matrix will look like the identity matrix again, with ones on the main diagonal and zeros everywhere else. Next, let’s try a thirty degree rotation around the $X$-axis:
… then the matrix will look like this:
Hmmm… weird. However, this makes more sense when we see the following equations:
So, this matrix is actually:
Unfortunately, this is not nearly as intuitive as the transform and scale examples above. However, once again we use it to write a formula. If we write the rotation around the $X$-axis as $R_{x}$, here’s the formula for rotation around the $X$-axis:
Similarly, here’s the formula for rotation around the $Y$-axis, $R_{y}$:
And finally, rotation around the $Z$-axis, $R_{z}$:
The World Matrix
As we’ve mentioned a few times, what’s important to us is the final position of an object in world space, since that’s what we see once the object is rendered. To help with calculating this, every object has a second transformation matrix, the world matrix, stored in
Object3D.matrixWorld
. There’s no difference, mathematically, between these two matrices. They’re both $4 \times 4$ transformation matrices, and when we create a mesh or any other scene object, both the local and world matrices are created automatically.
// when we create a mesh
const mesh = new Mesh();
// ... internally, three.js creates the local matrix and the world matrix
mesh.matrix = new Matrix4();
mesh.matrixWorld = new Matrix4();
The world matrix stores the position of the object in world space. If the object is a direct child of the scene, these two matrices will be identical, but if the object resides somewhere further down the scene graph, the local and world matrices will most likely be different.
To help us understand this, let’s look at our objects $A$ and $B$ from earlier once again:
Once again, we must force the matrices to update. Alternatively, you could call .render
and the matrices of all objects in the scene will be automatically updated.
If you recall from earlier, we calculated the final positions of $A$ and $B$ in world space and found that $A$ is at $(5, 0, 0)$, while $B$ ends up at $(8, 0, 0)$. Let’s examine how this works for each object’s local and world matrices. First up is $A$’s local matrix.
As we saw above, the position of an object on the $X$-axis is stored in the last column of the top row of its local matrix. Now, let’s look at $A$’s world matrix:
Since $A$ is a direct child of the scene, the local and world matrices are identical. Now, let’s take a look at $B$. First, the local matrix:
And finally, here is $B$’s world matrix:
This time, the local and world matrices are different since $B$ is not a direct child of the scene.
Working with Matrices Directly
Hopefully, this brief introduction has taken away some of the mystery of how matrices work. They are not as complicated as they look, rather, they are just a compact way of storing lots of numbers. However, keeping all those numbers in mind takes some practice, and doing calculations involving matrices by hand is tedious. Fortunately, three.js comes with many functions that allow us to work with matrices with ease. There are obvious functions like add, multiply, subtract, as well as functions to set and get the translation, rotation, or scale components of a matrix, and many others.
Working with the matrix directly, rather than setting .position
, .rotation
, and .scale
separately is almost never required, but it does allow for powerful manipulations of an object’s transform. Think of it like a superpower that you’ll unlock once you level up your three.js skills enough.
When used together, all of the properties we’ve encountered in this chapter - .position
, .rotation
, .scale
, .quaternion
, .matrix
, and .matrixWorld
- have tremendous expressive power, and enable you to create scenes like an artist with a paintbrush.
// when we create a mesh,
// or any other object derived from Object3D
// such as lights, camera, or even the scene itself
const mesh = new Mesh();
// ... internally, three.js creates
// many different objects to help us transform the object
mesh.position = new Vector3();
mesh.scale = new Vector3();
mesh.rotation = new Euler();
mesh.quaternion = new Quaternion();
mesh.matrix = new Matrix4();
mesh.matrixWorld = new Matrix4();
Learning how to use the .position
, .rotation
, and .scale
is a fundamental skill that you need to work with three.js. However, learning to use the .quaternion
and transformation matrices is an advanced skill that you don’t need to master immediately.