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.
Short answer: Use extractEulerAngleYXZ()
.
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.
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:
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:
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).
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).
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
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