Search code examples
c++quaternionsglm-matheuler-angles

How do I convert a Quaternion to Euler using glm and C++?


In my project, I use quaternions in my code, but I also want to be able to use Euler angles in other parts in my code because it's easier for me to use. But when I rotate the Y axis of a glm::quaternion, and use glm::eulerAngles to convert it to Euler, it outputs something like this:

Euler (0, 89, 0)
Euler (0, 90, 0)
Euler (180, 89, 180)
Euler (180, 88, 180)
Euler (180, 87, 180)

Code:

#include <iostream>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <chrono>
#include <thread>

int main() {
    glm::quat quaternion = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);

    // Loop to apply rotation and output Euler angles
    for (int i = 0; i < 360; ++i) {
        // Rotate quat
        glm::quat rotationQuat = glm::angleAxis(glm::radians(1.0f), glm::vec3(0.0f, 1.0f, 0.0f));
        quaternion = rotationQuat * quaternion;

        // Convert the quaternion to Euler angles
        glm::vec3 eulerAngles = glm::eulerAngles(quaternion);

        // Convert radians to degrees
        eulerAngles = glm::degrees(eulerAngles);

        // Output Euler angles
        std::cout << "Euler Angles: " << eulerAngles.x << ", " << eulerAngles.y << ", " << eulerAngles.z << std::endl;

        // Sleep
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }

    return 0;
}

Euler angles can be represented in two different ways when converting from a quaternion, I just can't figure out how to make the Y axis go to 91, 92, etc instead of making the X and Z axis 180, I would prefer the X and Z axes not be changed.


Solution

  • Short answer: Use extractEulerAngleYXZ().

    Background

    First, the term "Euler angles" potentially covers a wide range of possible ways to represent rotations, as explained in the Wikipedia article. Based on the question, I infer that what you're after is the angles often called "yaw", "pitch", and "roll", applied in that order w.r.t. the local (rotated) axes, and for pitch to range over 180 degrees while yaw and roll range over 360 degrees.

    Available APIs

    The OpenGL Mathematics library (GLM) has a module called GLM_GTX_euler_angles that offers a wide variety of representations, similar in scope to what the Wikipedia article describes. In particular, it offers yawPitchRoll() to convert from yaw/pitch/roll to a 4D matrix, and extractEulerAngleYXZ() to invert that operation. The latter is what I think you're after.

    In this module's nomenclature, the X axis is "right", the Y axis is "up", and the Z axis is "forward". "YXZ" refers to rotations applied in that order:

    • First, rotate about Y=up, i.e., "yaw".
    • Second, rotate about (local/rotated) X=right, i.e., "pitch".
    • Third, rotate about (local/rotated) Z=forward, i.e., "roll".

    Meanwhile, the GLM_GTC_quaternion module contains eulerAngles(), which the code in the question uses. This function advertises itself as returning a vector of pitch, yaw, and roll. Its behavior is equivalent to extractEulerAngleZYX() in the GLM_GTX_euler_angles module, meaning it assumes the order is:

    • Z=forward, i.e., "roll".
    • Y=up, i.e., "yaw".
    • X=right, i.e., "pitch".

    In addition to this being (to me) an odd order, the convention in these systems is evidently for the second angle to have a range of only 180 degrees, while the first and third range over 360 degrees. Consequently, as shown in the question, when the input yaw exceeds 90 degrees, this ZYX system flips Z and X rather than letting Y continue past 90. In contrast, the YXZ system lets yaw continue to increase, as desired (and pitch is instead constrained to 180 degrees).

    Can all three axes range over 360 degrees?

    No, because then there would be multiple representations of the same rotation. For example, consider yawing by 180, then pitching 180, then rolling 180: you end up back in the original orientation (y=0, p=0, r=0). If all three axes' ranges are 360, then every orientation has two representations (ignoring coordinate singularities due to "gimbal lock"). A normalized quaternion has only one representation for a given orientation, so half of a 360/360/360 space must necessarily be unused. The variety of functions in GLM_GTX_euler_angles let you choose which half to discard.

    If your application requires a 360/360/360 representation for some reason, then you must track additional information, something like a history of rotation. But that requires something beyond a single quaternion (or single pure rotation matrix).

    Solution code

    The procedure for extracting yaw/pitch/roll with GLM is thus:

    // Convert a quaternion to a vector whose components represent the yaw,
    // pitch, and roll angles, in that order (both in the vector, and in
    // rotation application order), in radians.
    glm::vec3 toYawPitchRoll(glm::quat const &q)
    {
        // Convert the quaternion to a rotation matrix in order to call the
        // next function.  (Presumably this entire procedure could be
        // optimized by inlining and simplifying the arithmetic.)
        glm::mat4 m = mat4_cast(q);
    
        // Now get the YPR angles.  The `euler_angles.hpp` header contains
        // many similar functions, but this one is the inverse of its
        // `yawPitchRoll` function.
        float y, p, r;
        glm::extractEulerAngleYXZ(m, y, p, r);
    
        // Package them as a vector for interface similarity with
        // `glm::eulerAngles` (but the order is different!).
        return glm::vec3{y, p, r};
    }
    

    To call the above, we need to include another header and define a symbol to allow it to be used:

    #define GLM_ENABLE_EXPERIMENTAL
    #include <glm/gtx/euler_angles.hpp>    // glm::extractEulerAngleYXZ
    

    Fixed output

    When the above function is substituted into the original code in place of glm::eulerAngles, the output is:

    Euler Angles: 1, -0, 0
    Euler Angles: 2, -0, 0
    [...]
    Euler Angles: 88.9999, -0, 0
    Euler Angles: 89.9999, -0, 0
    Euler Angles: 90.9999, -0, 0     <-- this is where the problem was
    Euler Angles: 91.9999, -0, 0
    [...]
    Euler Angles: 179, -0, 0
    Euler Angles: 180, -0, 0
    Euler Angles: -179, -0, 0
    Euler Angles: -178, -0, 0
    [...]
    Euler Angles: -3, -0, -0
    Euler Angles: -2.00001, -0, -0
    Euler Angles: -1.00002, -0, -0
    Euler Angles: -2.78543e-05, -0, -0