Using Quaternions for OpenGL Rotations

25,942

Solution 1

All you have done is effectively implement Euler angles with quaternions. That's not helping.

The problem with Euler angles is that, when you compute the matrices, each angle is relative to the rotation of the matrix that came before it. What you want is to take an object's current orientation, and apply a rotation along some axis, producing a new orientation.

You can't do that with Euler angles. You can with matrices, and you can with quaternions (as they're just the rotation part of a matrix). But you can't do it by pretending they are Euler angles.

This is done by not storing angles at all. Instead, you just have a quaternion which represents the current orientation of the object. When you decide to apply a rotation to it (of some angle by some axis), you construct a quaternion that represents that rotation by an angle around that axis. Then you right-multiply that quaternion with the current orientation quaternion, producing a new current orientation.

When you render the object, you use the current orientation as... the orientation.

Solution 2

Quaternions represent orientations around 3D compound axes. But they can also represent 'delta-rotations'.

To 'rotate an orientation', we need an orientation (a quat), and a rotation (also a quat), and we multiply them together, resulting in (you guessed it) a quat.

You noticed they are not commutative, that means the order we multiply them in absolutely matters, just like for matrices. The order tends to depend on the implementation of your math library, but really, there's only two possible ways to do it, so it shouldn't take you too long to figure out which one is the right one - if things are 'orbiting' instead of 'rotating', then you have them the wrong way around.

For your example of yaw and pitch, I would build my 'delta-rotation' quaternion from yaw, pitch and roll angles, with roll set to zero, and then apply that to my 'orientation' quaternion, rather than doing the rotations one axis at a time.

Share:
25,942
GarrickW
Author by

GarrickW

I am an amateur programmer with some experience in C++ and Python. I'm most interested in game programming, as well as AI, procedural generation of things like terrain or societies, and artificial life simulations.

Updated on July 05, 2022

Comments

  • GarrickW
    GarrickW almost 2 years

    So I'm writing a program where objects move around spacesim-style, in order to learn how to move things smoothly through 3D space. After messing around with Euler angles a bit, it seems they aren't really appropriate for free-form 3D movement in arbitrary directions, so I decided to move on to what seems to be best for the job - quaternions. I intend for the object to rotate around its local X-Y-Z axes at all times, never around the global X-Y-Z axes.

    I've tried to implement a system of rotation using quaternions, but something isn't working. When rotating the object along a single axis, if no previous rotations were undertaken, the thing rotates fine along a given axis. However, when applying one rotation after another has been performed, the second rotation is not always along the local axis it's supposed to be rotating along - for instance, after a rotation of about 90° around the Z axis, a rotation around the Y axis still takes place around the global Y axis, rather than the new local Y axis which is aligned with the global X axis.

    Huh. So let's go through this step by step. The mistake must be in here somewhere.

    STEP 1 - Capture Input

    I figured it would be best to use Euler angles (or a Pitch-Yaw-Roll scheme) for capturing player input. At the moment, arrow keys control Pitch and Yaw, whereas Q and E control Roll. I capture player input thus (I am using SFML 1.6):

        ///SPEEDS
        float ForwardSpeed = 0.05;
        float TurnSpeed = 0.5;
    
        //Rotation
        sf::Vector3<float> Rotation;
        Rotation.x = 0;
        Rotation.y = 0;
        Rotation.z = 0;
        //PITCH
        if (m_pApp->GetInput().IsKeyDown(sf::Key::Up) == true)
        {
            Rotation.x -= TurnSpeed;
        }
        if (m_pApp->GetInput().IsKeyDown(sf::Key::Down) == true)
        {
            Rotation.x += TurnSpeed;
        }
        //YAW
        if (m_pApp->GetInput().IsKeyDown(sf::Key::Left) == true)
        {
            Rotation.y -= TurnSpeed;
        }
        if (m_pApp->GetInput().IsKeyDown(sf::Key::Right) == true)
        {
            Rotation.y += TurnSpeed;
        }
        //ROLL
        if (m_pApp->GetInput().IsKeyDown(sf::Key::Q) == true)
        {
            Rotation.z -= TurnSpeed;
        }
        if (m_pApp->GetInput().IsKeyDown(sf::Key::E) == true)
        {
            Rotation.z += TurnSpeed;
        }
    
        //Translation
        sf::Vector3<float> Translation;
        Translation.x = 0;
        Translation.y = 0;
        Translation.z = 0;
    
        //Move the entity
        if (Rotation.x != 0 ||
            Rotation.y != 0 ||
            Rotation.z != 0)
        {
            m_Entity->ApplyForce(Translation, Rotation);
        }
    

    m_Entity is the thing I'm trying to rotate. It also contains the quaternion and rotation matrices representing the object's rotation.

    STEP 2 - Update quaternion

    I'm not 100% sure this is the way it's supposed to be done, but this is what I tried doing in Entity::ApplyForce():

    //Rotation
    m_Rotation.x += Rotation.x;
    m_Rotation.y += Rotation.y;
    m_Rotation.z += Rotation.z;
    
    //Multiply the new Quaternion by the current one.
    m_qRotation = Quaternion(m_Rotation.x, m_Rotation.y, m_Rotation.z);// * m_qRotation;
    
    m_qRotation.RotationMatrix(m_RotationMatrix);
    

    As you can see, I'm not sure whether it's best to just build a new quaternion from updated Euler angles, or whether I'm supposed to multiply the quaternion representing the change with the quaternion representing the overall current rotation, which is the impression I got when reading this guide. If the latter, my code would look like this:

    //Multiply the new Quaternion by the current one.
    m_qRotation = Quaternion(Rotation.x, Rotation.y, Rotation.z) * m_qRotation;
    

    m_Rotation is the object's current rotation stored in PYR format; Rotation is the change demanded by player input. Either way, though, the problem might be in my implementation of my Quaternion class. Here is the whole thing:

    Quaternion::Quaternion(float Pitch, float Yaw, float Roll)
    {
        float Pi = 4 * atan(1);
    
        //Set the values, which came in degrees, to radians for C++ trig functions
        float rYaw = Yaw * Pi / 180;
        float rPitch = Pitch * Pi / 180;
        float rRoll = Roll * Pi / 180;
    
        //Components
        float C1 = cos(rYaw / 2);
        float C2 = cos(rPitch / 2);
        float C3 = cos(rRoll / 2);
        float S1 = sin(rYaw / 2);
        float S2 = sin(rPitch / 2);
        float S3 = sin(rRoll / 2);
    
        //Create the final values
        a = ((C1 * C2 * C3) - (S1 * S2 * S3));
        x = (S1 * S2 * C3) + (C1 * C2 * S3);
        y = (S1 * C2 * C3) + (C1 * S2 * S3);
        z = (C1 * S2 * C3) - (S1 * C2 * S3);
    }
    
    //Overload the multiplier operator
    Quaternion Quaternion::operator* (Quaternion OtherQuat)
    {
        float A = (OtherQuat.a * a) - (OtherQuat.x * x) - (OtherQuat.y * y) - (OtherQuat.z * z);
        float X = (OtherQuat.a * x) + (OtherQuat.x * a) + (OtherQuat.y * z) - (OtherQuat.z * y);
        float Y = (OtherQuat.a * y) - (OtherQuat.x * z) - (OtherQuat.y * a) - (OtherQuat.z * x);
        float Z = (OtherQuat.a * z) - (OtherQuat.x * y) - (OtherQuat.y * x) - (OtherQuat.z * a);
        Quaternion NewQuat = Quaternion(0, 0, 0);
        NewQuat.a = A;
        NewQuat.x = X;
        NewQuat.y = Y;
        NewQuat.z = Z;
        return NewQuat;
    }
    
    //Calculates a rotation matrix and fills Matrix with it
    void Quaternion::RotationMatrix(GLfloat* Matrix)
    {
        //Column 1
        Matrix[0] = (a*a) + (x*x) - (y*y) - (z*z);
        Matrix[1] = (2*x*y) + (2*a*z);
        Matrix[2] = (2*x*z) - (2*a*y);
        Matrix[3] = 0;
        //Column 2
        Matrix[4] = (2*x*y) - (2*a*z);
        Matrix[5] = (a*a) - (x*x) + (y*y) - (z*z);
        Matrix[6] = (2*y*z) + (2*a*x);
        Matrix[7] = 0;
        //Column 3
        Matrix[8] = (2*x*z) + (2*a*y);
        Matrix[9] = (2*y*z) - (2*a*x);
        Matrix[10] = (a*a) - (x*x) - (y*y) + (z*z);
        Matrix[11] = 0;
        //Column 4
        Matrix[12] = 0;
        Matrix[13] = 0;
        Matrix[14] = 0;
        Matrix[15] = 1;
    }
    

    There's probably something in there to make somebody wiser than me cringe, but I can't see it. For converting from Euler angles to a quaternion, I used the "first method" according to this source, which also seems to suggest that the equation automatically creates a unit quaternion ("clearly normalized"). For multiplying quaternions, I again drew on this C++ guide.

    STEP 3 - Deriving a rotation matrix from the quaternion

    Once that is done, as per R. Martinho Fernandes' answer to this question, I try to build a rotation matrix from the quaternion and use that to update my object's rotation, using the above Quaternion::RotationMatrix() code in the following line:

    m_qRotation.RotationMatrix(m_RotationMatrix);
    

    I should note that m_RotationMatrix is GLfloat m_RotationMatrix[16], as per the required parameters of glMultMatrix, which I believe I am supposed to use later on when displaying the object. It is initialized as:

    m_RotationMatrix = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
    

    Which I believe is the "neutral" OpenGL rotation matrix (every 4 values together represent a column, correct? Again, I get this from the glMultMatrix page).

    STEP 4 - Display!

    Finally, we get to the function run each cycle for the object that is supposed to display it.

    glPushMatrix();
    
    glTranslatef(m_Position.x, m_Position.y, m_Position.z);
    glMultMatrixf(m_RotationMatrix);
    
    //glRotatef(m_Rotation.y, 0.0, 1.0, 0.0);
    //glRotatef(m_Rotation.z, 0.0, 0.0, 1.0);
    //glRotatef(m_Rotation.x, 1.0, 0.0, 0.0);
    
    //glRotatef(m_qRotation.a, m_qRotation.x, m_qRotation.y, m_qRotation.z);
    
    //[...] various code displaying the object's VBO
    
    glPopMatrix();
    

    I have left my previous failed attempts there, commented out.

    Conclusion - Sad panda

    That is the conclusion of the life cycle of player input, from cradle to OpenGL-managed grave.

    I've obviously not understood something, since the behavior I get isn't the behavior I want or expect. But I'm not particularly experienced with matrix math or quaternions, so I don't have the insight required to see the error in my ways.

    Can somebody help me out here?

  • GarrickW
    GarrickW about 12 years
    All right, so I've purged all angle storage from the Entity class. It now only stores a quaternion. How do I translate arrow key input into a quaternion without Euler angles, though? Do I have to calculate in real-time the axis that a given input will rotate the object around, given its current orientation (as represented by its quaternion)? Also, is the approach of extracting a rotation matrix from the quaternion appropriate, or unnecessary?
  • GarrickW
    GarrickW about 12 years
    I could simply pass one of [-1,0,1] to Entity for each of its local X-Y-Z axes and have Entity interpret that, but wouldn't that boil down to the same problem?
  • GarrickW
    GarrickW about 12 years
    Ah, I've just discovered the tutorial at arcsynthesis.org/gltut/Positioning/Tutorial%2008.html - I'll go rummage around in there for a moment.
  • GarrickW
    GarrickW about 12 years
    So I've read through the appropriate page, but there is something I'm still not sure about. It says "When we want to increase the pitch, for example, we will take the current orientation and multiply into it a quaternion that represents a pitch rotation of a few degrees." First, does this mean creating a quaternion from axis-angle, where the angle is the change in pitch (say 5 degrees) and the axes (x,y,z) are (1,0,0), regardless of the current orientation?
  • GarrickW
    GarrickW about 12 years
    Second, since quaternion multiplication is non-communtative, what if the player is inputting a pitch and roll change at the same time? Do I just make two separate offset quaternions, one for roll and one for pitch, and then multiply them together? What order should I multiply them in?
  • stil
    stil about 8 years
    "effectively implement Euler angles with quaternions". I did that also, and spent 2 days trying to solve my problem. After finding your answer I fixed my code in under 3 minutes. Damn.