Bites: Row/column major and row/column vector conventions
Row vs column major
Different math libraries will store matrices in either row or column major ordering. For example glm
and GLSL by default use column major ordering.
Row vs. column major is not a mathematical concept, it is a computing concept.
Row-major: Elements of a matrix are stored linearly in memory row-by-row one after another. Column-major: Elements of a matrix are stored linearly in memory column-by-column one after another.
Take the following matrix:
There are 2 ways we can store this in memory:
R1 R2 R3 R4
Row-major: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
C1 C2 C3 C4
Column major: [1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]
Why?
- A choice must be made at some point
- Your program's access patterns might benefit from one or the other, although you can pretty much always rewrite your code to take advantage either way
Regardless of which one you choose, you need only transpose the matrix to obtain the same result:
// Matrix4f's 16 arguments are stored in memory linearly, one after another
int main() {
Matrix4f M_row( // Row major ordering
1, 2, 3, 4
, 5, 6, 7, 8
, 9, 10, 11, 12
, 13, 14, 15, 16
);
Matrix4f M_col( // Column major ordering of the same matrix M
1, 5, 9, 13
, 2, 6, 10, 14
, 3, 7, 11, 15
, 4, 8, 12, 16
);
Vector4f v(1, 10, 100, 1000);
Vector4f vRowMajor = M_row * v;
Vector4f vColMajor = M_col.Transpose() * v;
// These give the same result!
printVectorCol(vRowMajor);
printVectorCol(vColMajor);
}
[4321
8765
13209
17653]
[4321
8765
13209
17653]
Simply choose one and be consistent.
Vectors treated as rows or columns
Another source of confusion is whether vectors are treated as row or column vectors. Usually math libraries do not make this distinction by name of the vector class or how the data is stored, but rather in how the vector object is used.
A vector of length 4
is essentially a matrix with dimension of either 1 x 4
(row vector) or 4 x 1
(column vector). According to the rules of matrix multiplication, the "inner" dimensions of the multiplication need to match up to be valid:
M
(4x4) * vcol
(4x1) results in a 4x1 column vector
vrow
(1x4) * M
(4x4) results in a 1x4 row vector
Again this is down to chosen convention. glm
treats vectors as column vectors that are multiplied from the right of the matrix (post-multiplication). Whereas DirectXMath treats vectors as row vectors that are multiplied from the left of the matrix (pre-multiplication).
Once again you can obtain equivalent results numerically by taking the transpose of the matrix before multiplying:
// Treat v as a 1x4 row vector and multiply m from the left
static Vector4f LeftMultRowVector(const Vector4f& v, const Matrix4f& m) {
// Dot each column with v to form new row vector
return Vector4f(
Dot(v, Vector4f(m.m00, m.m10, m.m20, m.m30))
, Dot(v, Vector4f(m.m01, m.m11, m.m21, m.m31))
, Dot(v, Vector4f(m.m02, m.m12, m.m22, m.m32))
, Dot(v, Vector4f(m.m03, m.m13, m.m23, m.m33))
);
}
Matrix4f M(
1, 2, 3, 4
, 5, 6, 7, 8
, 9, 10, 11, 12
, 13, 14, 15, 16
);
Vector4f v2(1, 11, 111, 1111);
Vector4f vcolRes = M * v2; // Treating v2 as a column vector
printVectorCol(vcolRes);
Vector4f vrowRes = LeftMultRowVector(v2, M.Transpose()); // Treating v2 as a row vector
printVectorRow(vrowRes); // Expect the same result, as a row vector instead
The results are the same.
[4800
9736
14672
19608]
[4800 9736 14672 19608]
Note that Vector4f
is simply 4 numbers, it makes no assumptions about itself when it comes to representing a row or column vector. The output above was printed differently for each result for visual aid, not due to any differences in the data structure. The choice of which one (row vs column vector) it represents depends on the context of the code. Here the *
overload for Matrix4f
was written as a vector multiply from the right, meaning the vector is treated as a column vector (dot product the vector with each row of the matrix).
inline Vector4f operator* (const Matrix4f& mat, const Vector4f& v) {
Vector4f res;
res.x = v.x * mat.m00 + v.y * mat.m01 + v.z * mat.m02 + v.w * mat.m03;
res.y = v.x * mat.m10 + v.y * mat.m11 + v.z * mat.m12 + v.w * mat.m13;
res.z = v.x * mat.m20 + v.y * mat.m21 + v.z * mat.m22 + v.w * mat.m23;
res.w = v.x * mat.m30 + v.y * mat.m31 + v.z * mat.m32 + v.w * mat.m33;
return res;
}
On the other hand, the function LeftMultRowVector()
from above treats the incoming vector as a row vector, and follows the matrix multiplication rules accordingly to produce the result (dot product each column of the matrix with the vector).
Handedness
Neither of these have anything to do with left/right-handedness. That is a separate discussion.