Jump to content
  • Advertisement
Sign in to follow this  
Starfox

OpenGL Matrix storage layout and multiplication order woes

This topic is 2129 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

As an exercise I'm implementing my own vector / matrix library and using it in scene rendering. I'm using OpenGL and the intention is to have a GL-compatible storage layout. My matrix data is a float[4][4] 2-d array indexed in row-major addressing. To make sure my generated matrices match the expected GL layout I use GLM ( http://glm.g-truc.net ) to generate equivalent matrices and compare the 16 consecutive floating point elements in memory at the starting address to make sure they're identical (within some epsilon), and all my tests succeed so far. Now for concatenating transforms, I use pre-multiplication with my row-major matrices, which should be identical to post-multiplication with column-major matrices ( see http://www.opengl.org/discussion_boards/showthread.php/167648-Matrix-stacks-and-post-multiplication?p=1182985#post1182985 for more details). I test that against GLM too and again it succeeds. In other words, if I have a GLM transformation matrix and a GLM projection matrix along with my own transform and projection matrices then the 16-element floating point memory layout of GLM_projection * GLM_transform is identical to the layout of My_transform * My_projection. Till this point everything works fine, and I can successfully upload the matrices to GLSL programs (I make sure that the transpose flag is false, so data isn't transposed by the GL driver) and use them for rendering just fine. 

 

Now here's my problem: I'm loading Collada files with scene nodes having parent-child relationships. Now for a given node the world transform matrix should be equal to ParentTransform * LocalTransform, since I'm using row-major matrices and pre-multiply, correct? That doesn't seem to work however. Rotating the parent object seems to cause the children to rotate around their local origins, and not properly rotate while being parented to the object. Switching the order of the operation to LocalTransform * ParentTransform solves that problem. 

 

Can someone explain this to me? I'm not merely interested in getting it to work, I want to find out the reason for this behavior - after all the whole thing is an exercise. 

Share this post


Link to post
Share on other sites
Advertisement
It is in my opinion easier to thing about this transformation orders if you also consider where the vector is multiplied and how it should be transformed. In your case you first want to apply the local transform and then move it using the parent transform. The correct transformation in your case is thus (v*L)*P and not (v*P)*L where v is the vector to transform, L is the local transformation and P the parent one.

Share this post


Link to post
Share on other sites

So you are recreating things, although these are inside a very nice library you are already using.

It seems you may have gotten the memory layout of your matrix different or doing the multiplies the other way around, but you didnt show code so noone can know. Even when you fix this you wont have learned much, just wasted time.

Share this post


Link to post
Share on other sites

It is in my opinion easier to thing about this transformation orders if you also consider where the vector is multiplied and how it should be transformed. In your case you first want to apply the local transform and then move it using the parent transform. The correct transformation in your case is thus (v*L)*P and not (v*P)*L where v is the vector to transform, L is the local transformation and P the parent one.

Wouldn't that imply that in column-major storage it would be World = Parent * Local then?

 

So you are recreating things, although these are inside a very nice library you are already using.

It seems you may have gotten the memory layout of your matrix different or doing the multiplies the other way around, but you didnt show code so noone can know. Even when you fix this you wont have learned much, just wasted time.

Yes. The first three words of my post are "as an exercise". I'm using GLM as a reference to compare to. To verify the memory layout I print the 16 float elements at the base pointer of both GLM's and my matrices.

 

Here's the matrix multiplication code:

[source]

 

const mat4x4 operator*(const mat4x4& a, const mat4x4& b)
{
    mat4x4 Result;
    //Set all elements in Result to zero
    Result.zero();
    for(int row = 0; row < 4; row++)
    {
        for(int column = 0; column < 4; column++)
        {
            for(int inner = 0; inner < 4; ++inner)
            {
                Result[row][column] += a[row][inner] * b[inner][column];
            }
        }
    }
    return Result;
}

[/source]

 

Like I mentioned, this seems to work with transform + projection transformations - as in, GLM's projection * transform is, in memory, equal to my own mat4x4 transform * projection. Prior to multiplication I of course verified that the layout of the operand matrices matches the layout of the GLM matrices in memory too.

 

Any ideas? I'm clearly missing something here since the result of the code isn't what I expect so either my code is wrong or my understanding of the process is wrong.

Share this post


Link to post
Share on other sites

How do you load the matrix data out of the collada files? Is it possible you've accidentally transposed the original collada data?

Share this post


Link to post
Share on other sites

I think your are mixing different concepts which should be separated. Column-major and row-major have nothing to do with the order of multiplication. They are simply different ways to store a matrix in memory. You can have column-major matrices and use row vectors or row-major matrices and use column vectors. The order of multiplication is given by the choice of row or column vectors. When you use column vectors you have to multiply the matrix on the left and then the transformations go from the right to the left. On the other hand, if you use row vectors, you have to multiply the matrix on the right and transformations go from left to right. So, in your case you have to compose the transformations as L*P while in OpenGL (and basically everywhere in mathematics) we usually do P*L. Have I misunderstood your post and your are already doing it?

Share this post


Link to post
Share on other sites

1. Seconding apatriarca's post especially w.r.t. the existence of 2 concepts that have to be distinguished: column vs. row vector matrices, and memory layout.

 

2. If I remember correctly, then Collada uses column vectors and row-major layout. (OpenGL originally uses column vectors and column-major layout, D3D row vectors and row-major layout; notice that OpenGL and D3D show the same sequence of numbers due to that symmetry, but Collada doesn't match that.)

 

3. 

Now for a given node the world transform matrix should be equal to ParentTransform * LocalTransform, since I'm using row-major matrices and pre-multiply, correct?

For column vectors, the order of forward kinematics is expressed as

   transform := parent * local

and for row vectors accordingly as

   transform := local * parent

 
There are 2 rules to remember for this kind of things:
 
a) For a matrix product you always go row-wise through the left matrix and column-wise through the right matrix. The count of columns in the left matrix has to be equal to the count of rows in the right matrix. Using row vectors then means that the order "vector by matrix" is the way to go. Similar, when using column vectors then "matrix by vector" is the correct order.
 
b) When applying a sequence of matrices on a vector, then a matrix closer to the vector (on which side ever) is applied in a co-ordinate system more local to those in which the original vector is given.
Edited by haegarr

Share this post


Link to post
Share on other sites

How do you load the matrix data out of the collada files? Is it possible you've accidentally transposed the original collada data?

 

I checked and that doesn't seem to be the case. I'm loading using Open Asset Import and I compared rotation values between a scene node in my engine and in Maya and they seem to match.

 

I think your are mixing different concepts which should be separated. Column-major and row-major have nothing to do with the order of multiplication. They are simply different ways to store a matrix in memory. You can have column-major matrices and use row vectors or row-major matrices and use column vectors. The order of multiplication is given by the choice of row or column vectors. When you use column vectors you have to multiply the matrix on the left and then the transformations go from the right to the left. On the other hand, if you use row vectors, you have to multiply the matrix on the right and transformations go from left to right. So, in your case you have to compose the transformations as L*P while in OpenGL (and basically everywhere in mathematics) we usually do P*L. Have I misunderstood your post and your are already doing it?

I'm using row vectors and v*M as opposed to column vectors and M*v - the end result matches the layout OpenGL expects ( see http://www.opengl.org/discussion_boards/showthread.php/167648-Matrix-stacks-and-post-multiplication?p=1182985#post1182985 for more info).

 

 

1. Seconding apatriarca's post especially w.r.t. the existence of 2 concepts that have to be distinguished: column vs. row vector matrices, and memory layout.

 

2. If I remember correctly, then Collada uses column vectors and row-major layout. (OpenGL originally uses column vectors and column-major layout, D3D row vectors and row-major layout; notice that OpenGL and D3D show the same sequence of numbers due to that symmetry, but Collada doesn't match that.)

 

3. 

Now for a given node the world transform matrix should be equal to ParentTransform * LocalTransform, since I'm using row-major matrices and pre-multiply, correct?

For column vectors, the order of forward kinematics is expressed as

   transform := parent * local

and for row vectors accordingly as

   transform := local * parent

 
There are 2 rules to remember for this kind of things:
 
a) For a matrix product you always go row-wise through the left matrix and column-wise through the right matrix. The count of columns in the left matrix has to be equal to the count of rows in the right matrix. Using row vectors then means that the order "vector by matrix" is the way to go. Similar, when using column vectors then "matrix by vector" is the correct order.
 
b) When applying a sequence of matrices on a vector, then a matrix closer to the vector (on which side ever) is applied in a co-ordinate system more local to those in which the original vector is given.

 

I think my problem here was that I misunderstood the mathematical order of transformations needed. I thought that given a vertex you'd transform it to the parent's coordinate space then transform the result by the child's coordinate space - point B in your post makes it sound like I have that backwards. Am I correct?

Share this post


Link to post
Share on other sites

I think my problem here was that I misunderstood the mathematical order of transformations needed. I thought that given a vertex you'd transform it to the parent's coordinate space then transform the result by the child's coordinate space - point B in your post makes it sound like I have that backwards. Am I correct?

Yep. So to say, the transformation from model to parent space is "more local" than the transformation from parent to global space. Hence model-to-parent has to be closer to the vector:

   v' := v * TMP * TPG

 

See that v is given in a local space usually called the model space. Then TMP transforms v into parent's space (which is also a local space, because it still is not the global space ;)). From here on TPG transforms the result of the former step into the global space. This is to be understood as

   v' :=  ( ( v * TMP ) * TPG )

but because the matrix product is associative (not to be confused with commutative) the parentheses can be set "as wanted" (and hence also dropped).
Edited by haegarr

Share this post


Link to post
Share on other sites

 


I think my problem here was that I misunderstood the mathematical order of transformations needed. I thought that given a vertex you'd transform it to the parent's coordinate space then transform the result by the child's coordinate space - point B in your post makes it sound like I have that backwards. Am I correct?

Yep. So to say, the transformation from model to parent space is "more local" than the transformation from parent to global space. Hence model-to-parent has to be closer to the vector:

   v' := v * TMP * TPG

 

See that v is given in a local space usually called the model space. Then TMP transforms v into parent's space (which is also a local space, because it still is not the global space ;)). From here on TPG transforms the result of the former step into the global space. This is to be understood as

   v' :=  ( ( v * TMP ) * TPG )

but because the matrix product is associative (not to be confused with commutative) the parentheses can be set "as wanted" (and hence also dropped).

 

Aha, it makes perfect sense now. Thanks a lot, and thanks to everyone who contributed to this thread.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!