In Defense Of Matrices

In which you learn why you bent over backwards to express transformations as matrices.

Recasting the three types of transformations as matrices required some steps that perversely seem to add more work to the GPU's load. You should be questioning why matrix representations are used in computer graphics. Following are a few of the reasons.

Common Interface

GLSL provides the type mat4 to represent a 4x4 matrix. This vertex shader expects a mat4 uniform and uses it to transform the incoming position attribute using matrix-vector multiplication:

uniform mat4 transformation;
in vec3 position;

void main() {
  gl_Position = transformation * vec4(position, 1.0);
}

The shader doesn't know or care if the matrix represents a translation, rotation, or scale. It doesn't matter which. Instead of having three different vertex shaders, as you did before, you have just one that takes the desired transformation in a 4x4 matrix. You have developed a unifying scheme for representating transformations.

Transformation Chain

So far you've only considered applying one transformation at a time. Very soon you will be sequencing transformations together in a chain: first you translate, then you rotate, then you translate again, and then maybe you scale...

Thanks to their common interface, each of these transformations can be expressed as one matrix in a matrix-matrix-matrix-...-vector product. In this vertex shader, two transformation matrices are passed in as uniforms:

uniform mat4 transformation1;
uniform mat4 transformation2;
in vec3 position;

void main() {
  gl_Position = transformation2 * transformation1 * vec4(position, 1.0);
}

The position vector is transformed first by transformation1 and then by transformation2. Composing these chains without matrices is hard.

Matrix Concatenation

One of the reasons that you use matrices is because the GPU can use them to calculate four dot products in parallel. That sounds great, but translation using vector addition is surely less work. A matrix-vector multiplication requires many more multipy and add instructions.

The payoff comes when you apply multiple transformations. The chain of matrix multiplications can be precomputed into a single 4x4 matrix. In this JavaScript code, three matrices are multiplied together on the CPU and then sent to the shader program:

const chain = scaleMatrix.multiplyMatrix(rotateMatrix).multiplyMatrix(translateMatrix);
shaderProgram.setUniformMatrix4('manyTransformations', chain);

The shader program receives the single matrix:

uniform mat4 chain;
in vec3 position;

void main() {
  gl_Position = chain * vec4(position, 1.0);
}

The matrix chain might represent 100 transformations. Yet the GPU will only execute a very small and constant number of multiply and add instructions.