Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

4x4 Matrix Pre-multiplication vs Post-multiplication

I have the following functions:

void Matrix::Scale(const float xScale, const float yScale, const float zScale)
{
    Matrix scaleMatrix;
    scaleMatrix.m_data[M11] = xScale;
    scaleMatrix.m_data[M22] = yScale;
    scaleMatrix.m_data[M33] = zScale;
    *this *= scaleMatrix;
}

void Matrix::Translate(const float xTranslation, const float yTranslation, const float zTranslation)
{
    Matrix translationMatrix;
    translationMatrix.m_data[M14] = xTranslation;
    translationMatrix.m_data[M24] = yTranslation;
    translationMatrix.m_data[M34] = zTranslation;
    *this *= translationMatrix;
}

And I'm unsure about the last lines of both functions. Should I be doing pre-multiplication, or post-multiplication (i.e. what I'm doing now). What implications does it have for the use of this class? I'm using the class with OpenGL, so any similarities to that would probably be useful.

Edit:

My shader code looks like this:

void main()
{
    gl_Position = vec4(v_xy, 0.0, 1.0) * v_ModelMatrix * v_ViewMatrix * v_ProjectionMatrix;
    f_uv = v_uv;
}

My matrix multiplication function looks like this:

// Row 1
result[M11] = lhs[M11] * rhs[M11]   +   lhs[M12] * rhs[M21]   +   lhs[M13] * rhs[M31]   +   lhs[M14] * rhs[M41];    // Column 1
result[M12] = lhs[M11] * rhs[M12]   +   lhs[M12] * rhs[M22]   +   lhs[M13] * rhs[M32]   +   lhs[M14] * rhs[M42];    // Column 2
result[M13] = lhs[M11] * rhs[M13]   +   lhs[M12] * rhs[M23]   +   lhs[M13] * rhs[M33]   +   lhs[M14] * rhs[M43];    // Column 3
result[M14] = lhs[M11] * rhs[M14]   +   lhs[M12] * rhs[M24]   +   lhs[M13] * rhs[M34]   +   lhs[M14] * rhs[M44];    // Column 4

// Row 2
result[M21] = lhs[M21] * rhs[M11]   +   lhs[M22] * rhs[M21]   +   lhs[M23] * rhs[M31]   +   lhs[M24] * rhs[M41];    // Column 1
result[M22] = lhs[M21] * rhs[M12]   +   lhs[M22] * rhs[M22]   +   lhs[M23] * rhs[M32]   +   lhs[M24] * rhs[M42];    // Column 2
result[M23] = lhs[M21] * rhs[M13]   +   lhs[M22] * rhs[M23]   +   lhs[M23] * rhs[M33]   +   lhs[M24] * rhs[M43];    // Column 3
result[M24] = lhs[M21] * rhs[M14]   +   lhs[M22] * rhs[M24]   +   lhs[M23] * rhs[M34]   +   lhs[M24] * rhs[M44];    // Column 4

// Row 3
result[M31] = lhs[M31] * rhs[M11]   +   lhs[M32] * rhs[M21]   +   lhs[M33] * rhs[M31]   +   lhs[M34] * rhs[M41];    // Column 1
result[M32] = lhs[M31] * rhs[M12]   +   lhs[M32] * rhs[M22]   +   lhs[M33] * rhs[M32]   +   lhs[M34] * rhs[M42];    // Column 2
result[M33] = lhs[M31] * rhs[M13]   +   lhs[M32] * rhs[M23]   +   lhs[M33] * rhs[M33]   +   lhs[M34] * rhs[M43];    // Column 3
result[M34] = lhs[M31] * rhs[M14]   +   lhs[M32] * rhs[M24]   +   lhs[M33] * rhs[M34]   +   lhs[M34] * rhs[M44];    // Column 4

// Row 4
result[M41] = lhs[M41] * rhs[M11]   +   lhs[M42] * rhs[M21]   +   lhs[M43] * rhs[M31]   +   lhs[M44] * rhs[M41];    // Column 1
result[M42] = lhs[M41] * rhs[M12]   +   lhs[M42] * rhs[M22]   +   lhs[M43] * rhs[M32]   +   lhs[M44] * rhs[M42];    // Column 2
result[M43] = lhs[M41] * rhs[M13]   +   lhs[M42] * rhs[M23]   +   lhs[M43] * rhs[M33]   +   lhs[M44] * rhs[M43];    // Column 3
result[M44] = lhs[M41] * rhs[M14]   +   lhs[M42] * rhs[M24]   +   lhs[M43] * rhs[M34]   +   lhs[M44] * rhs[M44];    // Column 4

I was under the impression that if you post-multiply your matrices (i.e. viewMatrix = transform * viewMatrix;) then your shader code needs to apply MVP in the opposite order that I currently have?

Edit2:

The summary table on http://scratchapixel.com/lessons/3d-basic-lessons/lesson-4-geometry/conventions-again-row-major-vs-column-major-vector/ is confusing me, as I'm using post-multiply with OpenGL (indicating column-major), yet my matrix is laid out in memory as row-major?

like image 877
Mark Ingram Avatar asked Jul 22 '13 10:07

Mark Ingram


2 Answers

You seem to be mixing two issues here which is I guess what the webpage on scratchapixel is trying to explain. From the information that is on the page you are referring to, things seem pretty clear but getting this type of stuff right in the mind is hard. You have the theory (what you do in mathematics with a pen and paper) and what you do with your implementation (C++). These are two different problems.

Mathematics: you can use two notations, either column or row major. As mentioned by GraphicsMuncher on this webpage as well, with row major vector, on paper, you need to write the vector-matrix multiplication vM where v is the row vector (1x4) and M your 4x4 matrix, why because you can mathematically only write [1x4]*[4x4], and not the other way around. Similarly if you use column, then the vector needs to be written down vertically, or in notation [4x1] (4 rows, 1 column). Thus, the multiplication with a matrix can only be written: [4x4][4x1]. The matrix is put in front of the vector: Mv. The first notation is called post-multiplication and the second (Mv) is called pre-multiplication (the matrix is in front). Now, as mentioned by GraphicsMuncher, if you need to transform a vector (or a point) then you need to pay attention to the order of multiplication, when you write them down ON PAPER. If you want to translate something with matrix T and then rotate with R and then scale with S, then in a column major world, you need to to write v' = S * R * T * v. In a row major world you need to write v' = v * T * R * S.

That's for the theory.

Computer: then comes the point when you decide to implement this in C++ say. The good thing about this is that C++ doesn't impose you anything about anything. You can map the values of your matrix's coefficients in memory the way you want, and you can write the code to perform a matrix multiplication by another matrix the way you want. Similarly how you access the coefficients for a vector-matrix multiplication is completely up to you.

You need to be make a clear distinction about how you map your coefficients in memory and what conventions you need to use from a mathematical point of you view to represent your vectors. These are two independent problems. For instance in you case, you probably declare your matrix class as an array of say 16 contiguous floats. That's fine. Where coefficients m14, m24, m34 represent the translation part of the matrix (Tx, Ty, Tz), so you assume your "convention" is row-major even though you are told to use OpenGL matrix convention which is said to be column-major. Here your confusion comes from the fact that the mapping of the coefficients in memory is different from the mental representation you are making yourself of a "column-major" matrix. You code "row" but you were said to use (from a mathematical point of view) "column", hence your difficulty to make sense of whether you do things right or wrong.

What's important is to see a matrix as a representation of a coordinate system defined by three axes, and a translation. Where and how you store this data in memory is completely up to you. Assuming the three vectors representing the three axes of the coordinate system are named AX(x,y,z), AY(x,y,z), AZ(x,y,z), and the translation vector is denoted by (Tx, Ty, Tz), then mathematically if you use column vector you have (latex not supported I guess):

    AXx AYx AZx Tx 
M = AXy AYy AZy Ty
    AXz AYz AZz Tz
     0   0   0  1 

The axes of the coordinates system are written vertically. Now if you use row-major:

    AXx AXy AXz 0 
M = AYx AYy AYz 0
    AZx AZy AZz 0
     Tx  Ty  Tz 1

The axes of the coordinate system are written horizontally. So the problem now when it comes to the computer world, is how you store these coefficients in memory. You can as well do:

float m[16] = { AXx, AXy, AXz, 0, AYx, AYy, AYz, 0, AZx, AZy, AZz, 0, Tx, Ty, Tz, 1}; 

Does it tell you though which convention you use? No. You can also write:

float m[16] = { AXx, AXy, AXz, Tx, AYx, AYy, AYz, Ty, AZx, AZy, AZz, Tz, 0, 0, 0, 1}; 

or

float m[16] = { AXx, AYx, AZx, Tx, AXy, AYy, AZy, Ty, AXz, AYz, AZz, Tz, 0, 0, 0, 1};

again, that doesn't give you a particular indication of which "mathematical" convention you use. You are just storing 16 coefficients in memory in different ways and that's perfectly fine as long as you know what that way is, so that you can access them appropriately later on. Now keep in mind that a vector multiplied by a matrix should give you the same vector whether you use a row- or column- mathematical notation. Thus what's important really is that you multiply the (x,y,z) coordinates of your vector by the right coefficients from the matrix, which requires the knowledge of how "you" have decided to store the matrix coefficient in memory:

Vector3 vecMatMult (Vector3 v,
    float AXx, float AXy, float AXz, float Tx,
    float AYx, float AYy, float AYz, float Ty,
    float AZz, float AZy, float AZz, float Tz) {
    return Vector3(
        v.x * AXx + v.y * AYx + v.z * AZx + Tx,
        v.x * AXy + v.y * AYy + v.z * AZy + Ty,
        v.x * AXz + v.y * AYz + v.z * AZz + Tz
}

EDIT: above code was wrong, fixed it now.

I wrote this function to underline the fact that no matter which convention you use, the resulting of the vector * matrix multiplication is just a multiplication and an addition between the vector's input coordinates and the coordinate system's axis coordinates AX, AY and AZ (regardless of the notation you use, and regardless of the way you store them in memory).

If you use:

float m[16] = { AXx, AXy, AXz, 0, AYx, AYy, AYz, 0, AZx, AZy, AZz, 0, Tx, Ty, Tz, 1};

You need to call:

vecMatMult(v, m[0], m[1], m[2], m[12], m[4], m[5], m[6], m[13], ...

If you use:

float m[16] = { AXx, AYx, AZx, Tx, AXy, AYy, AZy, Ty, AXz, AYz, AZz, Tz, 0, 0, 0, 1};

You need to call:

vecMatMult(v, m[0], m[4], m[8], m[3], m[1], m[5], m[9], m[10], ...

Does that tell you which convention you use? No. You just need to call the right coefficients in the right places when you do a vec * mat multiplication. And that's all there is to it, as disconcerting as it may seem.

Now things are slightly different when it comes to mat * mat multiplication. You can assume that the order in which you multiply the matrices is not the same. So R * S * T is not the same as T * S * R. The order indeed matters. Now again if you use "row major" then mathematically you need to write (using your notation):

mt11 = ml11 * mr11 + ml12 * mr21 + m13 * m31 + ml14 * mr41

where ml is the left hand matrix and mr the right hand one: mt = ml * mr. However note that i haven't been using brackets [] for the access indices because I don't want to suggest we are accessing elements stored in a 1D array here. We are just talking about the coefficients of matrices. If you want to write this in C++, then it all depends of how you have stored your coefficients in memory as suggested above.

Hope it helps.

like image 74
user18490 Avatar answered Nov 08 '22 21:11

user18490


Matrices are associative, which means

ABC = (AB)C = A(BC)

So it doesn't matter which matrices you actually multiply together first (AB vs BC), so long as you keep the order the same. Pre- vs post-multiplication is a matter of AB vs BA.

That being said, this question explains the order in which you should multiply scaling/rotation/translation if you want your transformations to come out the way you want. If you use row matrices instead of column matrices, you reverse the order and transpose each element. Here's a slideshow giving a better explanation (jump to slide 19).

Strictly in terms of matrices, column matrices are ordered T * R * S, or Translate * Rotate * Scale. So if you start with the identity I, post-multiplication (what you're doing) is correct. If you change to pre-multiplication, a transformation of X by L will end up as L * X, so in order to get T * R * S you'd flip the order in which you execute your calls.

like image 28
GraphicsMuncher Avatar answered Nov 08 '22 20:11

GraphicsMuncher