The Trouble With Quaternions
So earlier this week I decided to write up a 6DOF Quaternion Camera that allows full movement along Yaw / Pitch. After all, I’ve used Quats to great success on animation systems. Anyway after I coded up the camera, I quickly I discovered that if I applied rotation on one axis I was fine (for instance, simply rotated my Camera along Y-Axis). However, if I moved along both Yaw & Pitch then the resulting Quaternion concatenation would incur Roll and lead to undesired effects.
(Note: I’m using a custom Quaternion class that consists of 4 floats however its the equivalent of a D3DXQuaternion)
Code sample:
Quaternion q1 = Quaternion::getIdentity();
//pitch up
Vector3 up(1,0,0);
Quaternion temp(up, 3.14f); //radians
q1 = q1 * temp;
//Look at angle-axis
Vector3 q_axis;
float q_angle;
q1.to_axis_angle(q_axis, q_angle); //debug
//now rotate along Y-axis like a camera
Vector3 heading(0,1,0);
Quaternion temp2(heading, 1.28f);
q1 = q1 * temp2;
q1.to_axis_angle(q_axis, q_angle); //debug
Log("quat test #1 - angle-axis #1 %f %f %f %f\n",q_axis.x,q_axis.y,q_axis.z,q_angle);
Vector3 euler;
q1.getEulerAngles(euler);
Log("euler %f %f %f\n",euler.x,euler.y,euler.z);
This was the program output:
quat test #1 - angle-axis #1 0.802096 0.000476 0.597195 angle: 3.140315
euler 3.141136 0.001526 -1.280000
Notice, in the Euler conversion you can see a little bit of leakage into Roll. The problem is discussed in more detail here at StackOverflow forums. So it looks the general wisdom is to split up the incoming angles, track those separately, then generate the Quaternion from scratch every frame if you go this route. That really is a bloody shame however, since you are completely regenerating a Quaternion every frame. And worst, you later use this Quaternion to most likely rotate a view vector. If you use the traditional method to rotate a vector with a Quaternion you are not really using playing to its’ strength.
Code example:
Vector3 axis(0,1,0); //y-axis rotation
Quaternion q(axis, 1.28f); //angle in radians
q.rotate(ViewVector);
Funny thing is you can do the same exact thing with angle-axis like so (not to mention its faster to transform a Vector via a matrix):
Vector3 axis(0,1,0); //y-axis rotation
Matrix3 m(axis, 1.28f); //angle in radians
m.Mul(ViewVector);
However, since Quaternions are non-commutative you could first apply rotation along Y-axis and then pitch the quaternion via X-Axis. The resulting output in this case is a nice clean Quaternion like one might expect:
quat test #2 - angle-axis #1 0.000476 0.802096 -0.597195 angle: 3.140315
euler 1.280000 0.000000 3.140000
So, simply by applying the rotations in a different order we prevented the leakage. So it is possible to cleanly combine two quaternions and prevent unwanted leakage if you maintain the proper order. However, you can still get into trouble if you perform additional Quaternion math on this concatenated Quaternion later.
Here is an interesting trick though. If you take the Conjugate of the Unit Quaternion you will end up with an Inverse Quaternion. Take this Quaternion and generate a Matrix. If you later use this Matrix to get your Up & Right vectors you can continue to use concatenated Quaternions.
const Quaternion& GetRotation()
{
return m_rot;
}
void SetRotation(const Quaternion& q)
{
m_rot = q;
//Inside the actor/camera class, use the Quaternion inverse to generate a matrix. However, store off your original Quaternion.
Quaternion q1 = q;
q1.conjugate();
q1.normalize();
QuaternionToMatrix(q1, m_quaternionInvMatrix); //Matrix generated using Quaternion inverse
}
Now you have a matrix that was built using the Quaternion inverse. You can actually use this as an actors rotation matrix (like for the spaceship, etc) and put in a 4×4 Matrix along with Translation. The cool thing is that you can also use this Quaternion later and navigate around leakage like so.
void Pitch(float angle)
{
Vector3 dir = m_quaternionInvMatrix.GetRight();
dir.Normalize();
Quaternion t(dir, -angle); //angle-axis to Quaternion
Quaternion q = GetRotation();
q = q * t;
SetRotation(q);
}
Performing rotation along the Y-axis would be the exact same:
void RotateY(float angle)
{
Vector3 dir = m_quaternionInvMatrix.GetUp();
dir.Normalize();
Quaternion t(dir, angle); //angle-axis to quaternion
Quaternion q = GetRotation();
q = q * t;
SetRotation(q);
}
Now you have an Actor that can accurately accumulate Quaternions in a fairly predictable manner. While I used it, I didn’t notice any leakage into Roll and everything worked nicely. I searched around the net for other topics on this but didn’t find anything. However, keep in mind you could also just go with 3×3 rotation matrices as well. Like Quaternions, you will evade the gimbal lock issue and many will find they are very ease to use.
Code example of using angle-axis which also avoids Gimbal Lock and will may possibly be superior to the Quaternion alternative for this purpose. Additionally, you can perform vector interpolation and weigh that against Quaternion::slerp if you like
void Pitch(float angle)
{
Vector3 dir = m_orientation.GetRight();
dir.Normalize();
Matrix3 tm(dir, -angle); //angle-axis to matrix
m_orientation *= tm;
}



Recent Comments