Why Matrices
Recasting the three transformations as matrix-vector multiplications may seem counterproductive. After all, we padded the rows of the matrices with 0s and 1s to effectively cancel out terms in the dot products. Surely the GPU needlessly burns cycles adding 0 and multiplying by 1. So, why do so many graphics systems use matrices to represent transformations? Let's discuss a few of the reasons.
Common Interface
Matrices give each transformation a common interface. For example, the mat4
type in GLSL is used to store a 4×4 matrix representing an arbitrary transformation. 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);
}
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.
Transformation Chain
Since each transformation is a matrix multiplication, a series of transformations is just a chain of matrix multiplications. So far we've only tried performing one transformation at a time. But models in a complex scene pass through a gauntlet of transformations: first they are rotated, then scaled, then translated, then rotated again, and then maybe translated… Each of these transformations is 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);
}
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 such chains with just scalar operations is messy.
Matrix Concatenation
With a matrix multiplication, the graphics card performs four dot products in parallel. That sounds great, but translating with vector addition or scaling with vector multiplication surely requires fewer machine instructions.
The payoff of matrices is seen when we apply multiple transformations to a vector. The chain of matrix multiplications may be precomputed or concatenated into a single 4×4 matrix. For example, four transformations are bundled into one matrix in this GLSL code:
mat4 combo = rotate2 * translate * scale * rotate1;
vec4 transformedPosition = combo * position;
mat4 combo = rotate2 * translate * scale * rotate1; vec4 transformedPosition = combo * position;
Often these matrices are multiplied together once on the CPU rather than per-vertex or per-fragment in GLSL. In this JavaScript code, three matrices are multiplied together on the CPU and then sent to the shader program:
const combo = rotate2.
multiplyMatrix(translate).
multiplyMatrix(scale).
multiplyMatrix(rotate1);
shader.setUniformMatrix4('combo', combo);
const combo = rotate2. multiplyMatrix(translate). multiplyMatrix(scale). multiplyMatrix(rotate1); shader.setUniformMatrix4('combo', combo);
This code assumes that we've written a Matrix4
abstraction, which we haven't. JavaScript doesn't support overloading of the builtin operators, so we can't use *
to multiply matrices together.
The vertex shader receives the single matrix:
uniform mat4 combo;
in vec3 position;
void main() {
gl_Position = combo * vec4(position, 1.0);
}
uniform mat4 combo; in vec3 position; void main() { gl_Position = combo * vec4(position, 1.0); }
The concatenated matrix combo
might represent 100 transformations. Yet the graphics card will only execute a very small and constant number of multiply and add instructions.