So I've been playing around with OpenGL and then like in another thread I've opened, I made some simple transform function to make objects draw correctly given their positions and rotations.
It is all fun and games until I get to trying to use the whole 3 axis for rotation.
To cut it short, the order of the rotations matter because the matrix mul is not commutative so depending on your implementation, if you rotate x, y, z, say, you will have a different result than rotating z, y, x when you are rotated any amount in the y axis, in the extreme you are rotated 90 deg around y and the z axis rotation becomes equal to the x axis rotation.
I'm using a simplification of Anton's funcs, and they seem to be equal to the implementations in GLM too (tho GLM let's you define some macro and have slightly different impls).
My question is how to fix this, if possible. This is the rotation func I'm using and works pretty well except z rotation:
/* mat4 array layout 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 */ mat4 rotate_xyz_deg( const mat4& m, vec3 deg ) { float sx = sine(deg.x); float cx = cosine(deg.x); float sy = sine(deg.y); float cy = cosine(deg.y); float sz = sine(deg.z); float cz = cosine(deg.z); mat4 mx = { 1, 0, 0, 0, 0, cx, sx, 0, 0, -sx, cx, 0, 0, 0, 0, 1 }; mat4 my = { cy, 0, -sy, 0, 0, 1, 0, 0, sy, 0, cy, 0, 0, 0, 0, 1 }; mat4 mz = { cz, -sz, 0, 0, sz, cz, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; return mz * (my * (mx * m)); }
I believe this is called "gimbal lock", and one solution is to use Quaternions. I never done it myself so hopefully someone more qualified will help out.
https://en.wikipedia.org/wiki/Gimbal_lock#Loss_of_a_degree_of_freedom_with_Euler_angles
Oh thanks Simon.
So this is a known problem. What I still didn't find is a clear code for quaternion rotations, like you input angles and position and get the transform back.
I see a lot of talk about it only but not enough clear code. Do you know of a reference for this?
One solution to the gimbal lock I thought was using the lookAt() function, but if quaternions are really so much better then it is a good idea to just to learn this next.
Ok so here's how I think this could go after reading a bit about the quaternions:
1 - define a unit sphere centered at arbitraty 0,0,0;
2 - get new point given by polar coords with new rotations dx, dy, dz;
3 - calc the normal between past vector and new vector, which will be just past and present points;
4 - the normal is the quaternion rotation axis;
5 - the rotation angle you get from the two points;
6 - transform teh quaternion into the Quaternion-derived rotation matrix;
7- this is basically the same as my function rotate_xyz_deg()
I assume, so now this can directly be used for both camera and objects;
is this how ppl do this usually?
In the past I was recommended the book "3d math primer for graphics and game development" but I don't know if it's actually any good (I read the first few chapters of the first edition years ago and it seemed good but that was a long time ago). The second edition is available online and there is a chapter about quaternions.
These math primers literally will load you up with a bunch of information that is superfluous. This is the plague of most "math" books and wikipedia entries.
It would be much better if they just gave the functions and then broke them down explaining why the math works.
That said, thank you for the entry, it is interesting in that explains in the end why some approaches are better for packing data or avoiding error than others.
I'm still to find a simple function that rotates 3 axis without gimbal lock, my guess is that it doesn't exist actually so that's why I don't find it anywhere, the problem is non solvable.
In principle, it should be possible to find the next point B in the unit sphere given rotations rx, ry, rx, from previous point A, calculate the normal of the two vectors A, B, and then rotate around that normal with the angle formed by the A, B using the quaternion approach to get the final rotation matrix and transform it with the translation matrix to get view_matrix/model_matrix.
My problem now is how to find B given [A, rx, ry, rx].
One way would be to use Spherical coordinates, but the angles there are ambiguous depending on the order of rotation transforms.
Let me explain better, we have spherical coordinates
(notice that here I use opengl convention, z positive coming towards you, y positive up and x positive right)
float x = cosine(a)*sine(b); float y = sine(a); float z = cosine(a)*cosine(b);
where 'a' is the angle between i) the diagonal formed by linking the origin to the point(x,y,z) and ii) the plane xz
and 'b' is the angle between i) the diagonal formed by linking the projection of point(x,y,z) on the xz plane and the origin, and ii) the x (or z) axis
that's all good but how to describe 'a' and 'b' in terms of rx, ry, rx? if that can be done unequivocally, then I can do my approach, but I'm not that good at math I think because I cannot do it just yet
If you will construct Quaternion from these three angles, you won't prevent gimbal lock. Every time you have rotation with three angles (that's called Euler angles), you will run into gimbal lock - regardless whether you use matrix or quaternion.
The solution to that is not to keep angles. Keep rotations as matrix. Or quaternion. Does not matter which - both work the same. But keep your rotation in this representation all the time. And then apply relative rotation when you want to rotate to somewhere else.
But inputs comes in angles, for instance I get mouse position to rotate player camera up down, left right, as an increment/decrement in latitude/ longitude angles.
so how could I apply the z rotation matrix without using z rot angles if that's whta you mean?
Input does not come in angles. What comes from mouse is relative change - rotate by 10 degrees (or whatever) left/right/up/down. You need to only incrementally apply transformation to your existing transform.
So instead of calculating matrix/quaternion from all your three angles, you just multiply existing matrix/quaternion with relative rotation.
NewMatrix = OldMatrix * RotateZ(deltaX) * RotateX(deltaY);
(order may be different, depends on your conventions)
Just make sure the transformation order applies those rotations in local coordinate space, not global. That way you'll never get into gimbal lock.
Hm that's interesting, I'll try to make it work. Thanks.
Just a quickie, the initial transformation matrix, if I'm looking at negative z and I'm sitting at 0,0,0, what is its form?
With Euler angles, now I simplified everything to these functions below. Notice that I made it everything so that I negate the sign of all pos and rot in camera, so now it is more clear what is going on.
How would this look like with the rotation matrix approach?
void fill_matrices( mat4& T, mat4& Rx, mat4& Ry, mat4& Rz, const Entity& e ) { const float& px = e.px, py = e.py, pz = e.pz; T = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, px, py, pz, 1 }; float sx = sine(e.rot_X); float cx = cosine(e.rot_X); float sy = sine(e.rot_Y); float cy = cosine(e.rot_Y); float sz = sine(e.rot_Z); float cz = cosine(e.rot_Z); Rx = { 1, 0, 0, 0, 0, cx, -sx, 0, 0, sx, cx, 0, 0, 0, 0, 1 }; Ry = { cy, 0, sy, 0, 0, 1, 0, 0, -sy, 0, cy, 0, 0, 0, 0, 1 }; Rz = { cz, -sz, 0, 0, sz, cz, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; } mat4 transform_view(const Entity& e) { mat4 T, Rx, Ry, Rz; /// pay attention to negated Entity fill_matrices(T, Rx, Ry, Rz, -e); return Rx * Ry * Rz * T; } mat4 transform_model(const Entity& e) { mat4 T, Rx, Ry, Rz; fill_matrices(T, Rx, Ry, Rz, e); return T * Rz * Ry * Rx; }
ps: notice the order of matrix mul in trasnform_model(), not only T comes first left to right, but also Rz swaps with Rx, which makes rotations in the z axis experience gimbal lock, and y and x works pretty well (if reversed, then rotations in x would gimbal lock, etc)
I'm trying to make sense of this. The first problem is that, from the link that @simon gave, I don't understand how to fill up the orientation vectors to begin with.
Sure they are the orientation of the local axis system in regards to global/world space, but you would need to rotate all 3 local axis in respect to that space to being with, so it seems that I'm back to the same problem as above. (actually you would not need to rotate all of them, true, but ...)
I need to find spherical coordinates but using the angles in terms of the angle rotations [for the local space].
I'm not sure of any of this at all, it seems the problem is way harder than I thought (even fixing conventions like always rot x, then y then z seems eluding)
I've found in Anton's early example some snippets and now just made a first quaternions rotation pipe.
It did fix the gimbal lock problem, but it introduced another weird problem I've spent 2 days trying to fix to no avail.
The composed rotations tend to drift away, that is, even if you return to the origin of rotations, your screen will be now bent and you gotta do the opposite pattern of rotations to revert the drift.
This is not present when I just pass the full angles to normal rotation matrices.
As a matter of fact, because of the quaternion drift problem, I tried to use full angles instead of incremental, but this introduces yet ANOTHER problem, some angle of rotation makes the screen shake non stop.
Geez, this thing is truly quite hard to work with.
Here's my quaternion pipe
mat4 q_rotation_mat(versor& q_ROT, vec3& X_axis, vec3& Y_axis, vec3& Z_axis, float dRot_X, float dRot_Y, float dRot_Z) { versor q_rotx = versor(-dRot_X, X_axis.v[0], X_axis.v[1], X_axis.v[2]); versor q_roty = versor(-dRot_Y, Y_axis.v[0], Y_axis.v[1], Y_axis.v[2]); versor q_rotz = versor(-dRot_Z, Z_axis.v[0], Z_axis.v[1], Z_axis.v[2]); q_ROT = q_rotx * q_ROT; q_ROT = q_roty * q_ROT; q_ROT = q_rotz * q_ROT; X_axis = update_axis(q_ROT, { 1, 0, 0 } ); Y_axis = update_axis(q_ROT, { 0, 1, 0 } ); Z_axis = update_axis(q_ROT, { 0, 0,-1 } ); mat4 ROT_mat = quat_to_mat4( q_ROT ); return ROT_mat; } mat4 q_transform_view(Camera& cam) { auto R = q_rotation_mat(cam); //cam.q_ROT = q_normalize(cam.q_ROT); vec3 cam_pos = vec3(cam.entity.pos); cam_pos = cam_pos + cam.orientation.X_axis * cam.move_X; cam_pos = cam_pos + cam.orientation.Y_axis * cam.move_Y; cam_pos = cam_pos + cam.orientation.Z_axis * -cam.move_Z; cam.entity.px = cam_pos.x; cam.entity.py = cam_pos.y; cam.entity.pz = cam_pos.z; cam.reset(); const float& px = cam.entity.px, py = cam.entity.py, pz = cam.entity.pz; mat4 T = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, px, py, pz, 1 }; return inverse( R ) * inverse( T ); } vec3 update_axis(versor q, vec3 v) { float w = q.deg; float x = q.i; float y = q.j; float z = q.k; auto a = 1 - 2 * y * y - 2 * z * z; auto b = 2 * x * y + 2 * w * z; auto c = 2 * x * z - 2 * w * y; auto d = 2 * x * y - 2 * w * z; auto e = 1 - 2 * x * x - 2 * z * z; auto f = 2 * y * z + 2 * w * x; auto g = 2 * x * z + 2 * w * y; auto h = 2 * y * z - 2 * w * x; auto i = 1 - 2 * x * x - 2 * y * y; vec3 ret; ret.x = a * v.x + d * v.y + g * v.z; ret.y = b * v.x + e * v.y + h * v.z; ret.z = c * v.x + f * v.y + i * v.z; return ret; }