Into Vertex Shaders Addendum 1: Matrix Math and You

Szenia Zadvornykh
7 min readJun 18, 2017

Matrices are a crucial part of both 2D and 3D graphics development. If you are unfamiliar with this topic, it can seem quite daunting. But you can go a long way with just a high level understanding of the related concepts and a good math library, which Three.js provides.

There are many tutorials and videos explaining how matrices work, but this post will instead focus on how you can use matrices and vectors in Three.js without understanding any of the internals.

The most common matrix you will be working with is a 4 by 4 matrix called a transformation matrix. Similar to the CSS transform property, the types of transformation that can be represented by this matrix include translation (change in position), rotation and scale.

Think of the transformation matrix as a magic black box. If you put a 3D point (called a vector) into this box and “shake” it, a transformed vector will come out. Let’s look at an example.

Say we have a vector with the value{x: 20, y: 20, z: 0} and a matrix representing a translation of {x: 10, y: 40, z: 0}. Putting the vector into the matrix will result in a transformed vector with the value of {x: 30, y: 60, z: 0}.

Translation (t).

If we have more vectors we can apply the same transformation by putting them into the same matrix.

Multiple Translations (t).

Applying the same transformation to different vectors is a common scenario in graphics development. Vectors often represent shapes: 3D geometries and 2D polygons. Applying a matrix transformation to such a shape will transform it without changing the relative positions of the points. This is called affine transformation.

Representation in Code

The equivalent of putting a vector into a matrix is multiplying the vector by said matrix.

transformedVector = vector * transformationMatrix;

In JavaScript, we cannot use operators (+, -, *,/) on custom classes like matrices and vectors. Instead Three.js provides a robust API for working with these classes. The Three.js equivalent of the multiplication above is as follows:

var vector = new THREE.Vector3(20, 20, 0);
var matrix = new THREE.Matrix4();
matrix.makeTranslation(10, 40, 0);vector.applyMatrix4(matrix);

Note than the original vector is now the transformed vector because applyMatrix4 modifies the vector directly.

By default, a matrix does not represent any transformation. Applying the default matrix, called an identity matrix, will return the same vector you put in. The matrix.makeTranslation(x, y, z)method is part of the Three.js API. It will make the matrix represent a translation based on the supplied arguments. If you ever need to reset a matrix, you can do so by calling matrix.identity().

The API also provides similar methods for rotation and scale. Scale is fairly straightforward, with a method called matrix.makeScale(x, y, z). Rotation is a little more involved, simply because rotation in 3D can get a little complicated. Three.js provides the following methods for rotation:

matrix.makeRotationX(angle);
matrix.makeRotationY(angle);
matrix.makeRotationZ(angle);
matrix.makeRotationAxis(axis, angle);
matrix.makeRotationFromEuler(euler);
matrix.makeRotationFromQuaternion(quaternion);

Note that all angles in Three.js are measured in radians, which represent rotation in PI, with 360 degrees being equal to 2 PI.

The method matrix.makeRotationAxis takes a THREE.Vector3 representing the axis of rotation, and an angle around that axis. It is a condensed version of the three methods above, and provides a way to represent a rotation around an irregular axis. The two lines below are equivalent:

matrix.makeRotationX(Math.PI);
matrix.makeRotationAxis(new THREE.Vector3(1, 0, 0), Math.PI);

A THREE.Euler represents rotation around the x, y, and z axes. This is the most common way of representing rotation. A THREE.Quaternion is an alternate way of representing rotation, based on an axis and an angle.

Depending on what you need to do, and the data you have available, you may end up working with any of these methods. Three.js also provides methods to easily convert between the different representations of rotation.

Finally, the Three.js api provides a method to create a matrix representing a combination of translation, rotation, and scale: matrix.compose.

var translation = new THREE.Vector3();
var rotation = new THREE.Quaternion();
var scale = new THREE.Vector3();
var matrix = new THREE.Matrix4();matrix.compose(translation, rotation, scale);

Stacking boxes

One of the great things about boxes is that they can be stacked. Similarly, matrices can be “stacked” to create a representation of a combined transformation.

Rotation (r), Scale (s) and Translation (t).

Moreover, this stack can be “squished” into a single matrix. This allows us to represent more complex transformations while using the same amount of numbers. Neat!

The equivalent of stacking matrices and squishing the stack into a single matrix is multiplication.

combinedMatrix = rotationMatrix * scaleMatrix * translationMatrix;

The Three.js API provides 2 methods for multiplying matrices: matrix.multiply(otherMatrix)and matrix.multiplyMatrices(matrixA, matrixB). The first method multiplies the matrix by another matrix. The second method sets the matrix to the result of matrixA * matrixB.

var rotationMatrix = new THREE.Matrix4().makeRotation(...);
var scaleMatrix = new THREE.Matrix4().makeScale(...);
var translationMatrix = new THREE.Matrix4().makeTranslation(...);
var combinedMatrix = new THREE.Matrix4();combinedMatrix.multiply(rotationMatrix);
combinedMatrix.multiply(scaleMatrix);
combinedMatrix.multiply(translationMatrix);

Multiplying 3 matrices that represent a rotation, scale and translation will result in the same matrix as calling matrix.compose with equal transform values.

Pre-multiplication and Post-multiplication

When working with numbers, multiplying a list will yield the same result no matter the order of multiplication.

1 * 2 * 3 * 4 = 24
4 * 3 * 2 * 1 = 24

This does not apply to matrices. As such, you always have to pay close attention to the order of multiplication. Consider the example below.

Translation (t) after Rotation (r).
Translation (t) before Rotation (r).

Think of it this way: even though the combined transformation is “squished” into a single matrix, the transformations are still applied in the order they were originally stacked. Cool!

Multiplying your matrix by another matrix is known as post-multiplication. Multiplying another matrix by your matrix is know as pre-multiplication. Note that matrix.multiply(otherMatrix) always post-multiplies, while matrix.multiplyMatrices(matrixA, matrixB) can do both.

A common use case for multiplying matrices is traversing up and down a scene graph. A scene graph is a data structure consisting of nodes with children, where each child is also a node that can have children of its own. Many graphics libraries, including Three.js, use a scene graph. The HTML DOM shares many traits of a scene graph as well.

When you transform a node in a scene graph, all of its children get the same transformation applied to them. Any of these children can have a transformation of their own, which is usually relative to their parent node. If you ever need to determine the transformation of a nested node relative to the root, this can be calculated easily by recursively pre-multiplying parent transformations until you reach the root node.

// start with the transformation for this node
var matrixWorld = new THREE.Matrix4().copy(node.matrix);
// get the first parent
var parent = node.parent;
do {
// pre-multiply
matrixWorld.multiplyMatrices(parent.matrix, matrixWorld);
// get next parent
parent = parent.parent;
}
while (parent);

In Three.js, the transformation of a child relative to the scene is stored in a property called matrixWorld.

The inverse of a matrix

When working with numbers, you can ‘undo’ a transformation. Consider the following:

4 * 5 = 20;
20 / 5 = 4;

This does not apply to matrices. You cannot divide a vector by a matrix, but you can do something similar by multiplying a vector by the inverse of the same matrix.

transformedVector = originalVector * matrix;inverseMatrix = matrix.inverse();untransformedVector = transformedVector * inverseMatrix;// untransformedVector == originalVector

The inverse of a transformation matrix represents the opposite transformation.

Three.js provides a method to calculate the inverse of a matrix:

var matrix = new THREE.Matrix4();// apply transformation ...var inverseMatrix = new THREE.Matrix4();matrix.getInverse(inverseMatrix);

Among other things, the inverse of a matrix is used when working with a camera object in a 3D scene.

As you can see, matrices are a powerful and versatile tool. They can represent a wide range of simple or complex transformations. Vectors can then be transformed using the same procedure, regardless of what kind of transformation the matrix represents.

There is much more to matrices than covered here, but I hope this post has given you some insight into their power, and how it can be tapped to deal with common scenarios in graphics development.

--

--

Szenia Zadvornykh

Creative coder with an interest in art and game design. Tech Lead @dpdk NYC.