9

In attempt to avoid angle lock when performing rotations I've tried to switch over to Quaternions. Somehow, I'm still managing to reach gimbal lock.

I'm not sure if its due to the math I've implemented, or a design error, so please point out if I should change my approach for my object coordinates.

Each of my objects hold an X,Y,Z value, and a pitch,yaw,roll value. When I change a rotation value, the object recalculates its vertices based on the above information. This logic is as follows:

    // vertex array
    vertices[x] -= /*Offset by origin point*/;

    // Quat.'s representing rotation around xyz axes
    Quaternion q1 = Quaternion(glm::vec3(1,0,0),pitch);
    Quaternion q2 = Quaternion(glm::vec3(0,1,0),yaw); 
    Quaternion q3 = Quaternion(glm::vec3(0,0,1),roll); 

    // total rotation
    Quaternion TotalRot = ( (q3 * q2) * (q1) ); 

    // conversion of original coordinates to quaternion
    Quaternion Point1 = Quaternion(0, vertices[x].x(), vertices[x].y(), vertices[x].z()); 

    // resulting rotated point
    Quaternion Point2 = Quaternion( (TotalRot * Point1) * TotalRot.inverse() );

    // placing new point back into vertices array
    vertices[x] = QVector3D(round(Point2.v.x),round(Point2.v.y),round(Point2.v.z));
    vertices[x]+= /*Undo origin point offset*/;

"vertices[]" is the objects vertex array. The origin point offset commented out above is just so that the object is rotated around the proper origin point, so it is shifted relative to 0,0,0 since rotations occur around that point (right?).

I have a pictorial representation of my problem, where I first yaw by 90, pitch by 45, then roll by -90, but the roll axis became parallel to the pitch axis:

problemexample

Edit:

I tried multiplying those 3 axis quaternions together, then multiplied by a 4x4 matrix, followed by multiplying that by my vertex point, but I still gimbal lock/reach singularity!

    Quaternion q1 = (1,0,0,pitch);
    Quaternion q2 = (0,1,0,yaw);
    Quaternion q3 = (0,0,1,roll);
    Quaternion qtot = (q1*q2)*q3;
    Quaternion p1(0, vertices[x].x(), vertices[x].y(), vertices[x].z());
    QMatrix4x4 m;
    m.rotate(qtot);
    QVector4D v = m*p1;
    vertices[x] = QVector3D(v.x(),v.y(),v.z());
Yattabyte
  • 827
  • 8
  • 25

2 Answers2

10

Your issue is that even when you're using quaternions, you're still storing three pitch, yaw, and roll values, rather than a quaternion, to represent an object's orientation.

Here's how you should use quaternions for rotation here:

  1. Instead of storing X, Y, Z, pitch, yaw, roll for each object, instead store X, Y, Z, orientation in each object, where orientation is a quaternion starting at the initial value (0, 0, 0, 1), meaning no rotation. Storing pitch, yaw, and roll for each object is vulnerable to singularities (gimbal lock) because as small changes are added, one of the intermediate rotations (say, a pitch) could result in the object being parallel to a rotation axis (say, the yaw axis), so that the next rotation about that axis could fail.

  2. Then, as an object is rotated, determine the pitch, yaw, and roll for that object that occurred during that frame (assuming that your input device provides the rotation in that form), convert it to a quaternion, then pre-multiply that quaternion into the object's orientation quaternion. This approach is less vulnerable to singularities because the changes in the rotation are expected to be very small each frame.

  3. Don't modify the object's X, Y, and Z (your verticies array) directly after changing the orientation. Instead, when an object's orientation changes, create a new rotation matrix to serve as part of the object's world transformation matrix (along with scaling and translation; for best results, calculate the world transform as translation * rotation * scaling).

  4. Every few frames, you should normalize the orientation quaternion to avoid undesirable changes in the orientation, which can happen due to rounding error.

Peter O.
  • 28,965
  • 14
  • 72
  • 87
5

If you take a Euler angles representation and turn it into quaternions just for doing the vector rotations, then you are still just using Euler angles. The gimbal lock problem will remain as long as you have Euler angles involved anywhere. You need to switch completely to quaternions and never go through a Euler angles representation anywhere at all if you want to completely eliminate that problem.

The basic way to do it is that you can use Euler angles at the far ends of your calculations (as the original input or as the ultimate output) if that is more convenient for you (e.g., Euler angles are often a more "human-readable" representation). But, use quaternions for everything else (and you can convert occasionally to rotation matrices, because they are more efficient for rotating vectors), and never do intermediary conversions to any of the "bad" rotation representations (Euler angles, axis-angles, etc.). The "gimbal-lock-free" benefits of quaternions and rotation matrices only apply if you stick with those two representations throughout all your calculations.

So, on one side, you have the singular representations (the formal term for "gimbal lock" is singularity):

  • Euler angles (of any kind, of which there are 12, by the way); and
  • axis-angle.

And on the other side, you have the singularity-free representations:

  • quaternions (more efficient memory-wise and for composition operations); and
  • rotation matrices (more efficient for applying rotations to vectors).

Whenever you operate (like putting several rotations together, or moving a rotating object) with a singular representation, you will have to worry about that singularity in every calculation you do. When you do those same operations with a singularity-free representation, then you don't have to worry about that (but you do have to worry about the constraints, which is the unit-norm constraint for quaternions and the proper orthogonality for rotation matrices). And of course, any time you convert to or from a singular representation, you have to worry about the singularity. This is why you should only do the conversions at the far ends of your calculations (when entering or leaving), and stick with the non-singular representations all through your calculations otherwise.

The only good purpose for Euler angles is for human readability, period.

Mikael Persson
  • 16,908
  • 6
  • 34
  • 49
  • So, at what point in my code should I switch over to a rotation matrix? After I combined the 3 rotated quats together? I really don't know what a quaternion rotation matrix looks like. – Yattabyte Feb 28 '15 at 02:39
  • 1
    I don't know anything about the broader context of that code that you posted, but I imagine that this is involved in some way with moving a 3D object in a computer game or something like that, maybe through physics calculations or user input (e.g., joystick). If it's through physics simulation, then you should do it all in terms of quaternions. And if it's from something like a joystick input, then that input should be immediately converted to a quaternion. Your objects should not store the rotations as euler angles (as you seem to do with "roll", "pitch" and "yaw" values). – Mikael Persson Feb 28 '15 at 03:16
  • I don't fully understand and this is bugging me. My objects have rotation values because their rotation can be changed on a whim. Something isn't clicking right in my head – Yattabyte Feb 28 '15 at 03:21
  • Yeah, but the rotation of your objects should be stored as quaternions directly, not as pitch-roll-yaw values. And when you say the rotation can be changed "on a whim"... what does that mean? Through user input? If so, what kind? Or through some motion calculations? ... the point is that you have to push that euler angles representation as far back as possible (if you need them at all). – Mikael Persson Feb 28 '15 at 03:36
  • The angles are just put into the quaternion, which then takes the angle `a = a/360 * pi*2`, used to determine the quaternion "w" component `w = cos(a/2)`, and the rotation axis vector `v = n* sin(angle/2)` I've even tried multiplying the quats for each axis together, then plugging that into a rotation matrix, multiplied by my vector. It still reaches "singularity"! I've edited my opening post with an example – Yattabyte Feb 28 '15 at 03:47
  • There will always be a singularity as long as you have euler angles (roll-pitch-yaw)... it does not matter how or when you convert them to quaternions. The singularity is a property of the euler angles. Converting them to something else does not eliminate the singularity... it's the fact that you use euler angles in the first place that makes the singularity exist. In other words, the only way to eliminate the singularity (or the possibility of a singularity arising) is to never have euler angles anywhere, period, no conversions, no nothing, just quaternions and rotation matrices all the way! – Mikael Persson Feb 28 '15 at 03:52
  • I understand the concept of the problem that you're explaining, but I don't understand the difference between storing a quaternion instead of eulers, when in order to use rotations with quaternions they need eulers. How else am I to define a quat,'s rotation without giving it an angle? I'm clearly missing something here... – Yattabyte Feb 28 '15 at 04:02
  • "in order to use rotations with quaternions they need eulers", that's nonsense, quaternions don't need eulers. One way to create a quaternion is to take a set of euler angles and convert them (as you did), but that's not necessary nor needed at all. You can define a quaternion on its own (just by giving 4 numbers that have a norm of 1), or through a conversion from euler angles, or from a rotation matrix, or from an axis-angle representation, or through the composition of other quaternions... – Mikael Persson Feb 28 '15 at 04:08
  • I just don't get it. This doesn't make sense! I need to use angles, but I don't want to reach singularity. You tell me that I can use angles as long as it is early enough, but you also say that I can never use them. Suggest something! What should I do? I need to specify rotation amounts. – Yattabyte Feb 28 '15 at 04:22
  • I somehow fixed it by replacing the angles with a quat, then offsetting the quat with angles. To me this seems just like what I was doing before. I really don't understand how this work compared to my old code. I suppose I will study over this more and respond with an answer – Yattabyte Feb 28 '15 at 04:55