3D Matrices explained.

M

Misty

Guest
I would like the matrix_build functions to be explained because I can't seem to figure them out.

When I put matrix_build(0,0,0,random(360),0,0,1,1,1)

the array indices 6,7 and 10 and 11 seem to be altered.

When I put matrix_build(0,0,0,0,random(360),0,1,1,1)

the array indices 1,3 and 5,6 seem to be altered.

when I put matrix_build(0,0,0,0,0,random(360),1,1,1)
the array indices 1,2 and 10,12 seem to be altered.

I cannot deduce any kind of pattern from this. What is the code behind the matrix?

What I am trying to do is replace my quaternions with matrix multiplications.
 

GMWolf

aka fel666
Firstly, we must pose the question:
Do you understand matrices?
And do you understand matrix transformation?
 

mMcFab

Member
This wikipedia page basically explains the pattern and why: https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations (One thing to note is that the indices may be different to what you assume as we use 4D matrices to allow translation)
If you're just doing transforms with the functions, you needn't concern yourself, but you do need to understand matrices in order to build them yourself.

I hope the link helps explain the rotation thing. It may be worth you looking for further resources on how matrices work, unfortunately I can't really recommend anything because I learned what I know in college
 

mMcFab

Member
WTF! You mean you found a college professor that could teach?
I suppose so - My maths teacher was really helpful and good at getting the facts and methods into our heads. Honestly, the stuff he taught me for the course has probably been more beneficial to my game development stuff than pretty much anything else
 
T

TimothyAllen

Guest
Honesty, most of professors I had people complain about were good.
Its just that the people complaining where uninterested and slept in class...
My Java teacher couldnt even get simple connect 4 ai to work... she teaches ai. Obviously she teaches theory from a book and doesnt know 💩💩💩💩.
 
M

Misty

Guest
This wikipedia page basically explains the pattern and why: https://en.wikipedia.org/wiki/Rotation_matrix#Basic_rotations (One thing to note is that the indices may be different to what you assume as we use 4D matrices to allow translation)
If you're just doing transforms with the functions, you needn't concern yourself, but you do need to understand matrices in order to build them yourself.

I hope the link helps explain the rotation thing. It may be worth you looking for further resources on how matrices work, unfortunately I can't really recommend anything because I learned what I know in college
Long story short, can I get a local rotation effect by multiplying matrices, and how would I do so.
 

mMcFab

Member
Long story short, can I get a local rotation effect by multiplying matrices, and how would I do so.
Indeed you can!

You can add transforms together with matrix_multiply
Code:
combinedMatrix = matrix_multiply(matrix1, matrix2)
I find it best to think of "matrix1" as the current transform and "matrix2" as the transform to be added. You can combine as many transforms into one matrix as you like, just be aware that the GML can get slow if there's a lot of them.

As for how, I'm probably going to need a little more information about what you want to achieve, otherwise I'm probably going to accidentally answer something completely differnt :p
I'd just need you to elaborate on "local rotation effect" a little - do you just want one transform to rotate drawing about a specfic point, or a rotation transform relative to another transform? Do you just need an object to spin around? Or something else?
I also might need to know for sure if this is in a 2D or 3D context (I assume 3D since you mentioned quaternions which aren't very commonly used in 2D afaik)
I only ask so I can be as helpful as possible!
 
M

Misty

Guest
Indeed you can!

You can add transforms together with matrix_multiply
Code:
combinedMatrix = matrix_multiply(matrix1, matrix2)
I find it best to think of "matrix1" as the current transform and "matrix2" as the transform to be added. You can combine as many transforms into one matrix as you like, just be aware that the GML can get slow if there's a lot of them.

As for how, I'm probably going to need a little more information about what you want to achieve, otherwise I'm probably going to accidentally answer something completely differnt :p
I'd just need you to elaborate on "local rotation effect" a little - do you just want one transform to rotate drawing about a specfic point, or a rotation transform relative to another transform? Do you just need an object to spin around? Or something else?
I also might need to know for sure if this is in a 2D or 3D context (I assume 3D since you mentioned quaternions which aren't very commonly used in 2D afaik)
I only ask so I can be as helpful as possible!
Basically, imagine a spaceship. The spaceship has a rotx roty and rotz. I want the spaceship to turn using local rotations, like pitch and yaw, so it turns on itself and no gimbal locked euler behavoir.
I tried this code m1=matrix_build(0,0,0,key_up,key_left,key_q,1,1,1) m2=matrix_build(0,0,0,rotx,roty,rotz,1,1,1) m3=matrix_multiply(m2,m1) rotx=m3[3] roty=m3[4] rotz=m3[5]. I converted everything to degtorad but no dice.
 

mMcFab

Member
Basically, imagine a spaceship. The spaceship has a rotx roty and rotz. I want the spaceship to turn using local rotations, like pitch and yaw, so it turns on itself and no gimbal locked euler behavoir.
I tried this code m1=matrix_build(0,0,0,key_up,key_left,key_q,1,1,1) m2=matrix_build(0,0,0,rotx,roty,rotz,1,1,1) m3=matrix_multiply(m2,m1) rotx=m3[3] roty=m3[4] rotz=m3[5]. I converted everything to degtorad but no dice.
Okay, degtorad shouldn't be necessary as the matrix_build function takes degrees, so that's one less think to worry about. I can aslo say with confidence that "rotx=m3[3] roty=m3[4] rotz=m3[5]" isn't going to yield any useful information from the matrices (decomposing matrices is usually a pain in the butt anyway), though I can see that it would be easy to assume that those indices would represent rotation values (unfortunately, they don't).
I'm really trying to think of the rotation, and I think I have an idea of what to do, but I have 2 problems - 1) I'm really tired and struggling to focus 2) I'd want to test it out first. One of my ideas is to use axis-angle transforms, though I don't think there's a built=in matrix function for that - there is a d3d_transform_set/add_rotation_axis for it and this compatibility script does it too, however:
Code:
/// @description d3d - Sets the transformation to a rotation around the axis indicated by the vector with the indicated amount.
/// @param xa the x component of the transform vector
/// @param ya the y component of the transform vector
/// @param za the z component of the transform vector
/// @param angle the angle to rotate the transform through the vector

// get the sin and cos of the angle passed in
var c = dcos(-argument3);
var s = dsin(-argument3);
var omc = 1 - c;

// normalise the input vector
var xx = argument0;
var yy = argument1;
var zz = argument2;
var length2 = sqr(xx) + sqr(yy) + sqr(zz);
var length = sqrt(length2);
xx /= length;
yy /= length;
zz /= length;

// build the rotation matrix
var mT;
mT[0] = omc * xx * xx + c;
mT[1] = omc * xx * yy + s * zz;
mT[2] = omc * xx * zz - s * yy;
mT[3] = 0;

mT[4] = omc * xx * yy - s * zz;
mT[5] = omc * yy * yy + c;
mT[6] = omc * yy * zz + s * xx;
mT[7] = 0;

mT[8] = omc * xx * zz + s * yy;
mT[9] = omc * yy * zz - s * xx;
mT[10] = omc * zz * zz + c;
mT[11] = 0;

mT[12] = 0;
mT[13] = 0;
mT[14] = 0;
mT[15] = 1;

var m = matrix_get( matrix_world );
var mR = matrix_multiply( m, mT );
matrix_set( matrix_world, mR );
The basic idea in my head is to build the axis transforms and multiply them together as well as determine the correct axis to rotate around each time by transforming a unit vector. I think this idea would allow relative rotation without gimbal locking issues, I'd still need to test it though.
Ok, I will probably try to make a solution tomorrow, assuming no-one else comes up with a solution in the meantime.

EDIT: I may have figured out a shorter and more elegant solution, still need to test it though.
As Fel666 said below, it would be better to stick to quaternions for this sort of thing.
I'm going to answer with respect to matrices anyway, since it's what the question is.
 
Last edited:

GMWolf

aka fel666
Just as a note: matrices are no replacement for quaternions.
If you are making a spaceship game: stick to quaternions.
 

mMcFab

Member
Okay, I think I've done it!
EditoTest.gif
The axes shown are local to the cone/pyramid-drawing object, the camera is static. All the controls rotate the object around it's local axes. You can rotate about all 3 axis at once, I just didn't in the gif to keep it clear that the object was rotating about it's local axes properly.

To keep things simple, here's a download to the .gmz of the project

I'm glad I didn't try this last night with that rotation_axis stuff - that was way too overcomplicated, whereas this is rather simple in comparison. Sleep is good!

Here's the important stuff to note anyway:
In create, we define the "current" transform matrix - we update this in step as well as use it to draw:
Code:
//Current rotation matrix - default with no rotation
current_matrix = matrix_build(0, 0, 0,
                              0, 0, 0,
                              1, 1, 1);
                            
//Define z, just in case
z = 0;
This matrix could have any rotation to start with, this only affects its initial orientation

In step, we determine the rotations we want to perform and build a matrix out of this. We then add the "current" matrix to this (It's important that the new matrix is the first argument - this is what allows the local rotation)
Code:
//Lazy control scheme - doesn't match completely perfectly (oops) but it basically works
//Roll
var REL_ROLL = keyboard_check(ord("E")) - keyboard_check(ord("Q"));
//Up-down pitch
var REL_PITCH = keyboard_check(ord("W")) - keyboard_check(ord("S"));
//Left-right yaw
var REL_YAW = keyboard_check(ord("D")) - keyboard_check(ord("A"));

var rotate_const = 5;//This just affects rotation speed
//YXZ transform order - Y is roll, X is pitch, Z is yaw
var matrix_to_add = matrix_build(0, 0, 0,
                                 REL_PITCH * rotate_const,
                                 REL_ROLL * rotate_const,
                                 REL_YAW * rotate_const,
                                 1, 1, 1);
                      
//I know I previously said the second matrix is the added one, and this is still true - I'm just bending it a little so the transforms become relative   
current_matrix = matrix_multiply(matrix_to_add, current_matrix);

/*
Although I have stated what is pitch, yaw and roll here, it doesn't really seem to matter (as long as you make it make sense to your input and models)
I just based them on the YXZ matrix build order of GameMaker
*/
Roll, pitch and yaw could be in any of the rotation arguments (depending on which direction is forward for your model), I just based them on the YXZ build order.

In draw, we add the matrix to the world transform, then add the translation separately to prevent any weirdness. Obviously, we reset this after drawing:
Code:
//Add the matrix to the current world transform
matrix_set(matrix_world, matrix_multiply(matrix_get(matrix_world), current_matrix));

d3d_transform_add_translation(x,y,z);

//Draw here

d3d_transform_set_identity();

If you need to determine direction vectors (for movement and stuff), you can just use "d3d_transform_vertex()".

That was fun to do! Hope this helps!
 
Last edited:

GMWolf

aka fel666
Okay, I think I've done it!
View attachment 6079
The axes shown are local to the cone-drawing object, the camera is static. All the controls rotate the object around it's local axes. You can rotate about all 3 axis at once, I just didn't in the gif to keep it clear that the object was rotating about it's local axes properly.

To keep things simple, here's a download to the .gmz of the project

I'm glad I didn't try this last night with that rotation_axis stuff - that was way too overcomplicated, whereas this is rather simple in comparison. Sleep is good!

Here's the important stuff to note anyway:
In create, we define the "current" transform matrix - we update this in step as well as use it to draw:
Code:
//Current rotation matrix - default with no rotation
current_matrix = matrix_build(0, 0, 0,
                              0, 0, 0,
                              1, 1, 1);
                            
//Define z, just in case
z = 0;
This matrix could have any rotation to start with, this only affects its initial orientation

In step, we determine the rotations we want to perform and build a matrix out of this. We then add the "current" matrix to this (It's important that the new matrix is the first argument - this is what allows the local rotation)
Code:
//Lazy control scheme - doesn't match completely perfectly (oops) but it basically works
//Roll
var REL_ROLL = keyboard_check(ord("E")) - keyboard_check(ord("Q"));
//Up-down pitch
var REL_PITCH = keyboard_check(ord("W")) - keyboard_check(ord("S"));
//Left-right yaw
var REL_YAW = keyboard_check(ord("D")) - keyboard_check(ord("A"));

var rotate_const = 5;//This just affects rotation speed
//YXZ transform order - Y is roll, X is pitch, Z is yaw
var matrix_to_add = matrix_build(0, 0, 0,
                                 REL_PITCH * rotate_const,
                                 REL_ROLL * rotate_const,
                                 REL_YAW * rotate_const,
                                 1, 1, 1);
                      
//I know I previously said the second matrix is the added one, and this is still true - I'm just bending it a little so the transforms become relative   
current_matrix = matrix_multiply(matrix_to_add, current_matrix);

/*
Although I have stated what is pitch, yaw and roll here, it doesn't really seem to matter (as long as you make it make sense to your input and models)
I just based them on the YXZ matrix build order of GameMaker
*/
Roll, pitch and yaw could be in any of the rotation arguments (depending on which direction is forward for your model), I just based them on the YXZ build order.

In draw, we add the matrix to the world transform, then add the translation separately to prevent any weirdness. Obviously, we reset this after drawing:
Code:
//Add the matrix to the current world transform
matrix_set(matrix_world, matrix_multiply(matrix_get(matrix_world), current_matrix));

d3d_transform_add_translation(x,y,z);

//Draw here

d3d_transform_set_identity();

If you need to determine direction vectors (for movement and stuff), you can just use "d3d_transform_vertex()".

That was fun to do! Hope this helps!
Huh, that simple? I though matrix transfor,ation happend in world space, not local space.
Thats great!
 
M

Misty

Guest
Cool, thanks guys.

Reason I didn't want to add quaternions is because it would lag the game and bloat it full of scripts and functions.

The GM Newton DLL is 6 years out of date, hanson seems to be on vacation for some reason, and so you cannot actually rotate a joint locally, you must use a quaternion solution or a matrix solution since it's rotations are based on global rotations.


There is only one puzzlement still left - how do I change the d3d_transform_vertex to return Euler rotations? Or does it return the Euler xyz by default and not the xyz vector (A vector is missing the omega rotations of the axis of the vector itself, a vector can only show 2 rotation values.)
 
Last edited:

mMcFab

Member
Yeah, this is pretty much one of the reasons to use quaternions - getting euler angles from a matrix is a right pain. Luckily, you can cover most things with the vectors (e.g bullet direction, and you can copy the matrix to orient them correctly)

d3d_transform_vertex only transforms the position of a vertex which hence works with vectors that start at O, so there is no way straightforward way to get the euler rotations with a single function - it's really the major way this matrix method fails.

One option would be to decompose the matrix into rotations (this can be trickier if the matrix contains scaling). I'm trying to get this to work, but it's taking a little time. If I can get it working, I'll post it (it works with some simple rotations, but loses it after I multiply it with anything - so it doesn't work with my demo. I've also read that decomposing matrices can also have some limitations if certain rotations are 90 degrees.

The other (potentially more reliable) method is to have at least two unit vectors (up and forward, probably - as you stated, you cannot find all the rotations with one vector, so this easiest solution is to add another!), transform the by the matrix and work backwards.
e.g. Transform both the forward and up vector by the matrix. Determine the pitch and yaw required to make the forward vector point that direction for it's default angle. Then, transform the up vector back and determine the roll by the angle difference.

This should result in a rotation matrix that has the same result, though the angles used may differ to what was originally input, assuming the vectors are not scaled.

I have managed to make a script for the second option. It's probably slow to call it a lot per step (lots of matrix and array stuff, though it ran pretty fast in my test - avg fps 2335 called every step, room speed 30), but it works quite reliably:
EditoTest.gif
The object on the left replicates the rotation of the object on the right from a matrix using the script. It looks to be rotated to the left only due to perspective. I also included some extra wiggling (Just helps show that this deals with more than one change of rotation at once)
Code:
///matrix_determine_rotation(matrix)

var WM = matrix_get(matrix_world);

var matrix = argument0;

matrix[12] = 0;//Remove translation - this clones the matrix, so no worries here.
matrix[13] = 0;
matrix[14] = 0;

matrix_set(matrix_world, matrix);

var angles = 0;

var tfVector = d3d_transform_vertex(0,1,0);

var tuVector = d3d_transform_vertex(0,0,1);

var yaw, pitch, roll;

//Z rotation
yaw = (point_direction(0,0,tfVector[0], tfVector[1])+90)%360;

//Rotate forward back by yaw
var tfy = (tfVector[1] * dcos(-yaw)) - (tfVector[0] * dsin(-yaw));

pitch = point_direction(0, 0, tfy, tfVector[2]);

//Set the matrix to reset the up vector (mostly)
//Have to split and multiply otherwise rotation order will be wrong and the script won't work.
matrix_set(matrix_world,  matrix_multiply(matrix_build(0,0,0, 0, 0, -yaw, 1, 1, 1), matrix_build(0,0,0,-pitch,0,0,1,1,1)));
tuVector = d3d_transform_vertex(tuVector[0], tuVector[1], tuVector[2]);

//Y rotation
roll = angle_difference(-90, point_direction(0,0,tuVector[0], tuVector[2]));

matrix_set(matrix_world, WM);

angles[0] = pitch; angles[1] = roll; angles[2] = yaw;

return angles;
Returns an array of [xrotation, yrotation, zrotation] which works as expected if you feed it straight back into a matrix_build. This script ONLY works with YXZ build order and rotation-only (it will work if scaled uniformly though) matrices.I̶'̶l̶l̶ ̶u̶p̶d̶a̶t̶e̶ ̶i̶t̶ ̶l̶a̶t̶e̶r̶ ̶t̶h̶o̶u̶g̶h̶.̶, Never mind, added a little bit to clear translation so it no longer causes problems.

I'll make a GMS2 compliant version at some point too.

Here are some final things I've been thinking about that could be problematic with this matrix method:
  • Tracking rotation is a pain in the butt (as established)
  • It works best with relative transforms, but could be problematic in other scenarios (I haven't tested in other possible situations yet)
  • Aiming the matrix to a specific orientation can be problematic, though still possible be using forward/right/up vectors in order to determine rotation needed
  • S̶t̶o̶r̶i̶n̶g̶ ̶t̶r̶a̶n̶s̶l̶a̶t̶i̶o̶n̶ ̶i̶n̶ ̶e̶i̶t̶h̶e̶r̶ ̶o̶f̶ ̶t̶h̶e̶ ̶m̶a̶t̶r̶i̶c̶e̶s̶ ̶w̶i̶l̶l̶ ̶b̶e̶ ̶p̶r̶o̶b̶l̶e̶m̶a̶t̶i̶c̶ ̶-̶ ̶i̶t̶'̶s̶ ̶b̶e̶t̶t̶e̶r̶ ̶t̶o̶ ̶a̶d̶d̶ ̶t̶r̶a̶n̶s̶l̶a̶t̶i̶o̶n̶ ̶s̶e̶p̶a̶r̶a̶t̶e̶l̶y̶ ̶w̶h̶e̶n̶ ̶d̶r̶a̶w̶i̶n̶g̶ Actually, I realised (and tested) and this isn't actually a problem. Translations in the relative matrix will translate relative to rotation, translations in the second will remain the same
As an extra little note, I thought I may as well state that the local transforming works because you effectively transform the object before it was ever in its current orientation. Weird to imagine, but that's kinda how it works (if I'd put the local transform matrix in the second argument of matrix_multiply, it would have rotated in world space)

Hopefully this helps with that last bit!

Now I have to fight the urge to make a space game - I mean, I already studied orbital mechanics, so why not? oh no. Maybe I'll make a DLL for Newton Dynamics too. Who knows yet.

EDIT: I updated the script so that translation doesn't break it (non-uniform scaling will still be problematic). Also corrected one of my notes on translation
 
Last edited:
M

Misty

Guest
Thanks for the help old chap. If this game works out, I feel like I owe you 1 percent of the revshare for this. Send me a PM if I forget.

As far as space games go, there is an emotional distance to it, the vasteness of space invades your subconscious. Working on them, is a bit like entering another zone or dimension.
 

mMcFab

Member
You're very welcome! By the way, I have updated the script slightly so that it can work with rotation matrices that also contain translation without breaking - non-uniform scaling is still a problem though.
I feel like I owe you 1 percent of the revshare for this
I appreciate the thought, though I'm not going to hold you to it - I'm happy enough just knowing I was able to help :D
 
M

Misty

Guest
I will try your new script and see how it works out. I tried the script from yesterday and it didn't seem to return any rotation angles other than 0. I didn't set up the matrix exactly the same as the example, I made 3 separate matrixes. I got the ship to rotate correctly with quaternion-like local rotations, but matrix_retrieve_rotations only returned angles of 0. I will try again today, both with the new script and setting up the rotations exactly the same as the example, and see if it works.
 
Hi Misty, I've been working on this project, whose purpose is to help visualize matrix math, take a look if interested:

https://app.box.com/s/8isblonss4voeoj9mhyf5ajq0jnqq601

To do a "local" rotation, what I do is reverse the order in which i multiply the "world" and transform matrices. In the project I linked above, just click on the "transform * world" button to do "local" rotations. What isn't yet included in the project are arbitrary axis rotations or view space relative rotations.
 
M

Misty

Guest
Hi Misty, I've been working on this project, whose purpose is to help visualize matrix math, take a look if interested:

https://app.box.com/s/8isblonss4voeoj9mhyf5ajq0jnqq601

To do a "local" rotation, what I do is reverse the order in which i multiply the "world" and transform matrices. In the project I linked above, just click on the "transform * world" button to do "local" rotations. What isn't yet included in the project are arbitrary axis rotations or view space relative rotations.
Cool, I will check that out when I get home.

Also, the thing is, I got Michael's script to work in GM, just had to delete the line in his script declaring the array as a local variable.
But the thing is, for some reason it won't work with GM Newton, gives buggy rotations, don't know why. Works fine in regular gm d3d.
 
Top