Matrices And You Part 2 : Behind 3D Transformations

Matrices, prise deux

Here’s the same matrix as before, but expressed differently :

{{ 1,  2,  3,  0 }
 { 5,  6,  7,  0 }
 { 9,  10, 11, 0 }
 { 13, 14, 15, 1 }}

Woah, big deal. You’ve added some curly brackets.

...yeah. But here’s another thing :

TV_4DVECTOR m1 = new TV_4DVECTOR(1, 2, 3, 0);
TV_4DVECTOR m2 = new TV_4DVECTOR(5, 6, 7, 0);
TV_4DVECTOR m3 = new TV_4DVECTOR(9, 10, 11, 0);
TV_4DVECTOR m4 = new TV_4DVECTOR(13, 14, 15, 1);

That’s the same matrix, just split in four.

In fact, in Direct3D, the cells of a matrix are accessed by a coordinate defined by [row, column], so that the cells of a TV6.5 TV_3DMATRIX are accessed by those coordinates :

{{ m11, m12, m13, m14 }
 { m21, m22, m23, m24 }
 { m31, m32, m33, m34 }
 { m41, m42, m43, m44 }}

And by the way, they’re called 3DMATRIX even if they are in fact 4D. Go figure.

Now that it’s settled, we can define matrix multiplication. Then you’ll see why I split it in the first place.

Behind 3D Transformations

When any 3D point is transformed :

  • Scaled
  • Moved
  • Rotated

It invoves matrix multiplication in the background.

It’s a wierd process, but it all makes sense in context. Here goes :

|             |   |             |   |             |
| A nD VECTOR | * | A nD MATRIX | = | A nD VECTOR |
|             |   |             |   |             |

[ X, Y, Z, 1 ] * | 1   2   3   0 | = [ X + 5Y + 9Z + 13, 2X + 6Y + 10Z + 14, 3X + 7Y + 11Z + 15, 1 ] 
                 | 5   6   7   0 |   
                 | 9   10  11  0 |   
                 | 13  14  15  1 |

That was probably confusing. But take it column-by-column :

[ A nD VECTOR ] * [ A COLUMN OF THE MATRIX AS A VERTICALLY TRANSPOSED nD VECTOR ] = A SINGLE VALUE

[ X, Y, Z, 1 ] * | 1  | = 1X + 5Y + 9Z + 13
                 | 5  |   
                 | 9  |   
                 | 13 |

As messed up as it might be, two 4D vectors multiplied together end up in a single value. Then, the matrix’s columns are processed one by one to give the resulting vector’s X, Y, Z components.

Where did that [ X, Y, Z, 1 ] vector come from?

  • It’s the position of the vertex, triangle or mesh we’re transforming, stored as a vector.

Why is the vector 4D anyway? Aren't positions usually 3D?

  • It’s 4D because everytime you supply a 3D vector to TV3D, in the background TV3D adds up that little 1 in the 4th component of the vector. Let’s call it W.
  • Its existance is necessary for translation/movement to work! I’ll explain later on.

Why is it multiplied in this order? Why not Matrix * Vector?

  • Because it’s simply impossible to multiply two matrices unless the number of colums of the first one is equal to the number of rows in the second one! Since we are multiplying a 1×4 vector to a 4×4 matrix, the only possible order is Vector * Matrix.
  • As a general rule, two matrices must verify the following property in order for multiplication to be valid: [a x n]*[n x b]

The matrix is split in columns now? Why did you split it in rows at the beginning then?

  • All in good time.

Some Deduction

Translation

OK, let’s start by the simpler stuff. How can translation, or movement, be encoded in a 4D matrix, or in a 4-length array of 4D vectors?

The W of the “position vector” we’re processing will always be 1, since TV3D forces it in. That means that whatever the rest of the matrix is, the fourth row of the matrix will always be added up to the resulting vector.

Note : At this point, I’ll remove the fourth column as well because it doesn’t mean anything in this context.

We could then simplify the multiplication operation like so :

[ X, Y, Z ] * | 1   2   3  | = [ X + 5Y + 9Z, 2X + 6Y + 10Z, 3X + 7Y + 11Z ] + [ 13, 14, 15 ]
              | 5   6   7  |   
              | 9   10  11 |

Already less numbers, woot.

So since [ 13, 14, 15 ] is just added to the result,... it looks like a translation indeed!

There you have it. The fourth row of a mesh transformation matrix is the following : [Translation.x, Translation.y, Translation.z, 1].

That’s one down, two to go. Scaling is the next easy one.

Scaling

If we clear all terms of the matrix except its diagonal, which means those :

[ X, Y, Z ] * | 1  0  0  | = [ X, 6Y, 11Z ]
              | 0  6  0  |   
              | 0  0  11 |

We can see that :

  • The X component of the 1st row in the matrix is multiplied with the X component of the vector
  • The Y component of the 2nd row is multiplied with the Y component of the vector
  • The Z component of the 3nd row is multiplied with the Z component of the vector

Say, that looks a lot like scaling. We want to scale the mesh on the three axis, by defined values... we can fit them into a matrix!

Rotation, on the other hand, needs a topic on its own...

Rotation Expressed With Vectors?

Now all the stuff about vectors above will come in useful.

Let’s not forget that the rows of the 3D matrix are vectors, with components too :

{{ m1.x, m1.y, m1.z },
 { m2.x, m2.y, m2.z },
 { m3.x, m3.y, m3.z }}

So in full added-up glory, this gives (I’ll transpose (^t) vertically the resulting vector for clarity) :

[ X, Y, Z ] * {{ m1.x, m1.y, m1.z }  = | m1.x * X + m2.x * Y + m3.x * Z |^t <- X
               { m2.x, m2.y, m2.z }    | m1.y * X + m2.y * Y + m3.y * Z |   <- Y
               { m3.x, m3.y, m3.z }}   | m1.z * X + m2.z * Y + m3.z * Z |   <- Z 

Isn’t wierd how we take components from X, Y and Z to get X? And same for Y and Z? You’ll see...

The Simplest Case

OK, let’s take it simply. What if there is no rotation applied to the mesh. We want it to look straight ahead, no transformation done on the initial mesh. Then the only way of getting that would be with 1’s on the diagonal :

[ X, Y, Z ] * {{ 1, 0, 0 }  = | X |^t <- X
               { 0, 1, 0 }    | Y |   <- Y
               { 0, 0, 1 }}   | Z |   <- Z 

Well that was just dumb. Why make the multiplication anyway, eh. But let’s analyze the vectors separately!

The First Row
  • We can see that its X component equals 1, but the rest is 0. Then, we can simply say that it’s facing right, and its length is 1.
The Second Row
  • We can see that its Y component equals 1, but the rest is 0. Then, we can simply say that it’s facing up, and its length is 1.
The Third Row
  • More of the same : its Z component equals 1, rest is 0. It’s facing forward, and its length is 1 as well.

Naming The Rows

Interesting... That would mean that the matrix is formed like this :

{ Normalized Right-Facing Vector,
  Normalized Up-Facing Vector,
  Normalized Forward-Facing Vector }

That’s a bit fast to draw such a big conclusion... but you can take it for granted; it’s the format of a rotation matrix.

What if we actually ARE rotating?

Alright, let’s take another fairly simple case : you’re looking UP, and above you it’s parallel to the ground BEHIND YOU. That would mean that at your right, it’s still the same RIGHT as normal.

In other words, you’re laying on your back and looking into the sky. You haven’t turned around so your right is still the world’s right, and above the top of your head is the ground that was behind you.

Then, logically :

  • The relative Right-Facing vector will point in positive X
  • The relative Up-Facing vector will point in negative Z
  • The relative Forward-Facing vector will point in positive Y

And here’s the rotation matrix for that situation :

[ X, Y, Z ] * {{ 1, 0,  0 }  = |  X |^t <- X
               { 0, 0, -1 }    |  Z |   <- Y
               { 0, 1,  0 }}   | -Y |   <- Z 

But you ain’t naïve, and you want proof of my hastily-explained vector algebra. There you go then :

If you have a point that was in front of you; let’s say your nose. It’s right in front of your eye, at (0, 0, 1) in the positive Z axis. What happens if we transform our body with that rotation matrix, including our nose? Where would it stand?

[ 0, 0, 1 ] * {{ 1, 0,  0 }  = | 0 |^t <- X
               { 0, 0, -1 }    | 1 |   <- Y
               { 0, 1,  0 }}   | 0 |   <- Z 

And it works! It stands just on top of us.

What if I want to make a custom rotation?

I hate to say this, but do it in degrees, or with quaternions. And you’ll have to learn it somewhere else.

As far as I know, rotation matrices aren’t practical when you want to arbitrarily rotate in the three axis, since you’d have to compute the sin/cos operations that TV3D does in the background for you when you use RotateX/Y/Z anyway.

But that is not to say that rotation matrices aren’t useful for TV3D programmers. Just keep reading.

Putting it all together

Now it would probably be a good idea to construct the actual transformation matrix, the composition of the three operations. And there are associated wierdness which we need to cover.

Doesn't the rotation matrix conflict with the scaling matrix?

In fact, they all conflict together. Now that we’ve learned about matrix multiplication, let’s see how much it’s really used.

The main utility of matrix multiplications is that they preserve transformations : you can multiply a series of matrices together so that you have a resulting matrix that does everything that the initial matrices did, but in one shot.

To get the transformation matrix, here are the steps :

[ Rotation Matrix ] * [ Scaling Matrix ] * [ Translation Matrix ] = [ Transformation Matrix ]

Do note that matrix multiplication is not a commutative operation; which means that M1 * M2 != M2 * M1. So that order is inflexible.­

We’ve only defined the rotation matrix... and we’ve defined the 3D one. So here’s the full operation, in 4D!

| Right.x    Right.y    Right.z    0 |   | Scale.x  0        0        0 |   | 1       0       0       0 |
| Up.x       Up.y       Up.z       0 | * | 0        Scale.y  0        0 | * | 0       1       0       0 |
| Forward.x  Forward.y  Forward.z  0 |   | 0        0        Scale.z  0 |   | 0       0       1       0 |
| 0          0          0          1 |   | 0        0        0        1 |   | Move.x  Move.y  Move.z  1 |

=

| Right.x*Scale.x     Right.y*Scale.y     Right.z*Scale.z     0 |
| Up.x*Scale.x        Up.y*Scale.y        Up.z*Scale.z        0 |  <= Full transformation matrix
| Forward.x*Scale.x   Forward.y*Scale.y   Forward.z*Scale.z   0 |
| Move.x              Move.y              Move.z              1 |

So you can see why I dropped the 4th column earlier... It just doesn’t apply on mesh transformations. It’s used for projection... but that’s out of this article’s scope.

Notice how 1’s were added on the diagonal of some matrices? Those were put for the same reason as why scaling components are placed on the diagonal; the 1’s act like global mulipliers for the whole column, which preserve what was there before. In fact, the “unit” matrix which does not do anything on whatever it is multiplied against is called the identity matrix, and has 1’s filled on its diagonal and 0’s everywhere else.

And remember when I said that the three vectors forming the rotation matrix need to be normalized? Well if they’re not, scaling is performed, because the matrix interprets non-unit-length (non-normalized) vectors as scaling!

Note : Thankfully, you never have to extract the rotation matrix or any other out of a composed transformation matrix in TV3D. There is usually a GetRotationMatrix method which returns only the 4D rotation matrix and even better, a GetBasisVectors method which returns the three normalized vectors that form the rotation matrix.

Fabular. Now what am I supposed to do with it?

Right. It doesn’t help you for custom rotation, it’s rather useless for setting scale and position since it’s so easy to go via SetScale and SetPosition, what does it help you do? Read more in part 3!

 
tutorialsarticlesandexamples/matrices_and_you2.txt · Last modified: 2013/11/22 13:32