A 4x4 matrix is the workhorse of 3D graphics because one matrix can hold a
translation, a rotation and a scale at once, and multiplying two matrices
chains their transforms. magic-pixels has a general Matrix class for any
size, and the fixed-size Mat2, Mat3 and Mat4, which
are what the scene graph and the renderer use. This page is about
Mat4.
Mat4.values is a Float32Array of 16 numbers in column-major order, the
layout GLSL expects. Written out, the array indices sit in the matrix like
this:
| 0 4 8 12 |
| 1 5 9 13 |
| 2 6 10 14 |
| 3 7 11 15 |
So the first four numbers are the first column, not the first row. That is
easy to get wrong when reading the set([...]) literals in the source: each
line of four in those literals is a column. The convention pays off in two
places: uniformMatrix4fv takes the array as is, and the columns have
meaning.
A transform matrix applied to a point p = (x, y, z, 1) computes
x * col0 + y * col1 + z * col2 + 1 * col3. Read that as: the first three
columns are where the X, Y and Z axes of the object end up, and the fourth
column is where its origin ends up. A translation matrix is the identity with
the offset in column 3 (indices 12, 13, 14). A scaling matrix stretches the
three axis columns. A rotation matrix replaces the three axis columns with
three new perpendicular unit vectors.
That reading also explains getPosition(): the position of an object is
column 3 of its world matrix, nothing to compute.
The fourth component of p is 1 for points so the translation applies. For
a direction vector it is 0, and the translation column drops out, which is
exactly what a direction needs.
Multiplying matrices A × B produces a matrix that applies B first, then
A. This is the source of most confusion with matrices, so it is worth
stating the two orders used in the library:
T × R × S. Scale first, then rotate, then translate.
That is what you want: scaling an object around its own origin before
moving it somewhere. Mat4.compose(position, rotation, scale) builds this
directly, without three separate matrices, by scaling the columns of the
rotation matrix and writing the position into column 3. The rotation can
be a Quaternion (what the scene graph passes) or Euler angles.Rx × Ry × Rz for Euler angles in "XYZ order". Applied to
a vector, the Z rotation happens first in world terms. The more useful
reading is intrinsic: rotate the object around its own X axis, then
around its new Y axis, then its new Z axis. Order matters because rotations
do not commute; the same three angles in a different order give a
different orientation. Euler angles also have two well-known limits:
three sequential rotations can lose a degree of freedom (gimbal lock),
and two orientations cannot be interpolated cleanly. Quaternions solve
both, and Mat4.rotationFromQuaternion(q) turns one into a rotation
matrix without any trigonometry; see Quaternions.parent.worldMatrix × localMatrix: apply the local
transform, then the parent's, then the grandparent's, and so on up the
tree.projectionMatrix × viewMatrix × modelMatrix × position. Read right to left: object space, world space, view space, clip
space.Every method on Mat4 writes into this and returns this:
a.multiply(b) changes a. A frame with a thousand objects composes and
multiplies a few thousand matrices, and allocating a new Float32Array for
each would make the garbage collector part of the frame budget. When the
original is still needed, a.clone().multiply(b) allocates once. The
scene graph relies on this: object.localMatrix and object.worldMatrix
are allocated once with the object and overwritten every frame.
invert() computes the inverse with the closed-form cofactor expansion for
4x4 matrices; a singular matrix (determinant zero, for example a scale of 0)
becomes the zero matrix rather than throwing. The camera needs it every
frame: the view matrix is the inverse of the camera's world matrix.
Normals need special care. Transforming a normal with the model matrix is
wrong as soon as the scale is non-uniform: stretch a sphere along X and the
normals must tilt towards the stretched axis, but the model matrix would
stretch them along it. The correct matrix is the inverse transpose of the
upper-left 3x3 of the model-view matrix, which is what
Mat3.setNormalMatrix(mat4) computes and the renderer uploads as
normalMatrix. For a pure rotation the inverse transpose is the matrix
itself, so nothing changes in the common case.