Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Quaternion based camera

I try to implement an FPS camera based on quaternion math. I store a rotation quaternion variable called _quat and multiply it by another quaternion when needed. Here's some code:

void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
    glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
              q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));

    _quat = q;
}

void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
    glm::quat q = glm::angleAxis(glm::radians(-upAngle), glm::vec3(1,0,0));
              q*= glm::angleAxis(glm::radians(rightAngle), glm::vec3(0,1,0));

    _quat *= q;
}

The application can request the orientation matrix via GetOrientation, which simply casts the quaternion to a matrix.

glm::mat4 Camera::GetOrientation() const
{
    return glm::mat4_cast(_quat);
}

The application changes the orientation in the following way:

int diffX = ...;//some computations based on mouse movement
int diffY = ...;

camera.OffsetOrientation(g_mouseSensitivity * diffX, g_mouseSensitivity * diffY);

This results in bad, mixed rotations around pretty much all the axes. What am I doing wrong?

like image 477
Pilpel Avatar asked Feb 15 '15 20:02

Pilpel


2 Answers

The problem is the way that you are accumulating rotations. This would be the same whether you use quaternions or matrices. Combining a rotation representing pitch and yaw with another will introduce roll.

By far the easiest way to implement an FPS camera is to simply accumulate changes to the heading and pitch, then convert to a quaterion (or matrix) when you need to. I would change the methods in your camera class to:

void Camera::SetOrientation(float rightAngle, float upAngle)//in degrees
{
    _rightAngle = rightAngle;
    _upAngle = upAngle;
}

void Camera::OffsetOrientation(float rightAngle, float upAngle)//in degrees
{
    _rightAngle += rightAngle;
    _upAngle += upAngle;
}

glm::mat4 Camera::GetOrientation() const
{
    glm::quat q = glm::angleAxis(glm::radians(-_upAngle), glm::vec3(1,0,0));
              q*= glm::angleAxis(glm::radians(_rightAngle), glm::vec3(0,1,0));
    return glm::mat4_cast(q);
}
like image 56
GuyRT Avatar answered Dec 03 '22 07:12

GuyRT


The problem

As already pointed out by GuyRT, the way you do accumulation is not good. In theory, it would work that way. However, floating point math is far from being perfectly precise, and errors accumulate the more operations you do. Composing two quaternion rotations is 28 operations versus a single operation adding a value to an angle (plus, each of the operations in a quaternion multiplication affects the resulting rotation in 3D space in a very non-obvious way).
Also, quaternions used for rotation are rather sensible to being normalized, and rotating them de-normalizes them slightly (rotating them many times de-normalizes them a lot, and rotating them with another, already de-normalized quaternion amplifies the effect).

Reflection

Why do we use quaternions in the first place?

Quaternions are commonly used for the following reasons:

  1. Avoiding the dreaded gimbal lock (although a lot of people don't understand the issue, replacing three angles with three quaternions does not magically remove the fact that one combines three rotations around the unit vectors -- quaternions must be used correctly to avoid this problem)
  2. Efficient combination of many rotations, such as in skinning (28 ops versus 45 ops when using matrices), saving ALU.
  3. Fewer values (and thus fewer degrees of freedom), fewer ops, so less opportunity for undesirable effects compared to using matrices when combining many transformations.
  4. Fewer values to upload, for example when a skinned model has a couple of hundred bones or when drawing ten thousand instances of an object. Smaller vertex streams or uniform blocks.
  5. Quaternions are cool, and people using them are cool.

Neither of these really make a difference for your problem.

Solution

Accumulate the two rotations as angles (normally undesirable, but perfectly acceptable for this case), and create a rotation matrix when you need it. This can be done either by combining two quaternions and converting to a matrix as in GuyRT's answer, or by directly generating the rotation matrix (which is likely more efficient, and all that OpenGL wants to see is that one matrix anyway).

To my knowledge, glm::rotate only does rotate-around-arbitrary-axis. Which you could of course use (but then you'd rather combine two quaternions!). Luckily, the formula for a matrix combining rotations around x, then y, then z is well-known and straightforward, you find it for example in the second paragraph of (3) here.
You do not wish to rotate around z, so cos(gamma) = 1 and sin(gamma) = 0, which greatly simplifies the formula (write it out on a piece of paper).

Using rotation angles is something that will make many people shout at you (often not entirely undeserved).
A cleaner alternative is keeping track of the direction you look at either with a vector pointing from your eye in the direction where you wish to look, or by remembering the point in space that you look at (this is something that combines nicely with physics in a 3rd person game, too). That also needs an "up" vector if you want to allow arbitrary rotations -- since then "up" isn't always the world space "up" -- so you may need two vectors. This is much nicer and more flexible, but also more complex.
For what is desired in your example, a FPS where your only options are to look left-right and up-down, I find rotation angles -- for the camera only -- entirely acceptable.

like image 24
Damon Avatar answered Dec 03 '22 09:12

Damon