There's a great writeup on this process by Mike Day:
https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf
It is also now implemented in glm, as of version 0.9.7.0, 02/08/2015. Check out the implementation.
To understand the math, you should look at the values that are in your rotation matrix. In addition, you have to know the order in which the rotations were applied to create your matrix in order to properly extract the values.
A rotation matrix from Euler angles is formed by combining rotations around the x-, y-, and z-axes. For instance, rotating θ degrees around Z can be done with the matrix
┌ cosθ -sinθ 0 ┐
Rz = │ sinθ cosθ 0 │
└ 0 0 1 ┘
Similar matrices exist for rotating about the X and Y axes:
┌ 1 0 0 ┐
Rx = │ 0 cosθ -sinθ │
└ 0 sinθ cosθ ┘
┌ cosθ 0 sinθ ┐
Ry = │ 0 1 0 │
└ -sinθ 0 cosθ ┘
We can multiply these matrices together to create one matrix that is the result of all three rotations. It's important to note that the order that these matrices are multiplied together is important, because matrix multiplication is not commutative. This means that Rx*Ry*Rz ≠ Rz*Ry*Rx
. Let's consider one possible rotation order, z-y-x. When the three matrices are combined, it results in a matrix that looks like this:
┌ CyCz -CySz Sy ┐
RxRyRz = │ SxSyCz + CxSz -SxSySz + CxCz -SxCy │
└ -CxSyCz + SxSz CxSySz + SxCz CxCy ┘
where Cx
is the cosine of the x
angle of rotation, Sx
is the sine of the x
angle of rotation, etc.
Now, the challenge is to extract the original x
, y
, and z
values that went into the matrix.
Let's first get the x
angle out. If we know the sin(x)
and cos(x)
, we can use the inverse tangent function atan2
to give us back our angle. Unfortunately, those values don't appear by themselves in our matrix. But, if we take a closer look at elements M[1][2]
and M[2][2]
, we can see we do know -sin(x)*cos(y)
as well as cos(x)*cos(y)
. Since the tangent function is the ratio of the opposite and adjacent sides of a triangle, scaling both values by the same amount (in this case cos(y)
) will yield the same result. Thus,
x = atan2(-M[1][2], M[2][2])
Now let's try to get y
. We know sin(y)
from M[0][2]
. If we had cos(y), we could use atan2
again, but we don't have that value in our matrix. However, due to the Pythagorean identity, we know that:
cosY = sqrt(1 - M[0][2])
So, we can calculate y
:
y = atan2(M[0][2], cosY)
Last, we need to calculate z
. This is where Mike Day's approach differs from the previous answer. Since at this point we know the amount of x
and y
rotation, we can construct an XY rotation matrix, and find the amount of z
rotation necessary to match the target matrix. The RxRy
matrix looks like this:
┌ Cy 0 Sy ┐
RxRy = │ SxSy Cx -SxCy │
└ -CxSy Sx CxCy ┘
Since we know that RxRy
* Rz
is equal to our input matrix M
, we can use this matrix to get back to Rz
:
M = RxRy * Rz
inverse(RxRy) * M = Rz
The inverse of a rotation matrix is its transpose, so we can expand this to:
┌ Cy SxSy -CxSy ┐┌M00 M01 M02┐ ┌ cosZ -sinZ 0 ┐
│ 0 Cx Sx ││M10 M11 M12│ = │ sinZ cosZ 0 │
└ Sy -SxCy CxCy ┘└M20 M21 M22┘ └ 0 0 1 ┘
We can now solve for sinZ
and cosZ
by performing the matrix multiplication. We only need to calculate the elements [1][0]
and [1][1]
.
sinZ = cosX * M[1][0] + sinX * M[2][0]
cosZ = coxX * M[1][1] + sinX * M[2][1]
z = atan2(sinZ, cosZ)
Here's a full implementation for reference:
#include <iostream>
#include <cmath>
class Vec4 {
public:
Vec4(float x, float y, float z, float w) :
x(x), y(y), z(z), w(w) {}
float dot(const Vec4& other) const {
return x * other.x +
y * other.y +
z * other.z +
w * other.w;
};
float x, y, z, w;
};
class Mat4x4 {
public:
Mat4x4() {}
Mat4x4(float v00, float v01, float v02, float v03,
float v10, float v11, float v12, float v13,
float v20, float v21, float v22, float v23,
float v30, float v31, float v32, float v33) {
values[0] = v00;
values[1] = v01;
values[2] = v02;
values[3] = v03;
values[4] = v10;
values[5] = v11;
values[6] = v12;
values[7] = v13;
values[8] = v20;
values[9] = v21;
values[10] = v22;
values[11] = v23;
values[12] = v30;
values[13] = v31;
values[14] = v32;
values[15] = v33;
}
Vec4 row(const int row) const {
return Vec4(
values[row*4],
values[row*4+1],
values[row*4+2],
values[row*4+3]
);
}
Vec4 column(const int column) const {
return Vec4(
values[column],
values[column + 4],
values[column + 8],
values[column + 12]
);
}
Mat4x4 multiply(const Mat4x4& other) const {
Mat4x4 result;
for (int row = 0; row < 4; ++row) {
for (int column = 0; column < 4; ++column) {
result.values[row*4+column] = this->row(row).dot(other.column(column));
}
}
return result;
}
void extractEulerAngleXYZ(float& rotXangle, float& rotYangle, float& rotZangle) const {
rotXangle = atan2(-row(1).z, row(2).z);
float cosYangle = sqrt(pow(row(0).x, 2) + pow(row(0).y, 2));
rotYangle = atan2(row(0).z, cosYangle);
float sinXangle = sin(rotXangle);
float cosXangle = cos(rotXangle);
rotZangle = atan2(cosXangle * row(1).x + sinXangle * row(2).x, cosXangle * row(1).y + sinXangle * row(2).y);
}
float values[16];
};
float toRadians(float degrees) {
return degrees * (M_PI / 180);
}
float toDegrees(float radians) {
return radians * (180 / M_PI);
}
int main() {
float rotXangle = toRadians(15);
float rotYangle = toRadians(30);
float rotZangle = toRadians(60);
Mat4x4 rotX(
1, 0, 0, 0,
0, cos(rotXangle), -sin(rotXangle), 0,
0, sin(rotXangle), cos(rotXangle), 0,
0, 0, 0, 1
);
Mat4x4 rotY(
cos(rotYangle), 0, sin(rotYangle), 0,
0, 1, 0, 0,
-sin(rotYangle), 0, cos(rotYangle), 0,
0, 0, 0, 1
);
Mat4x4 rotZ(
cos(rotZangle), -sin(rotZangle), 0, 0,
sin(rotZangle), cos(rotZangle), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
Mat4x4 concatenatedRotationMatrix =
rotX.multiply(rotY.multiply(rotZ));
float extractedXangle = 0, extractedYangle = 0, extractedZangle = 0;
concatenatedRotationMatrix.extractEulerAngleXYZ(
extractedXangle, extractedYangle, extractedZangle
);
std::cout << toDegrees(extractedXangle) << ' ' <<
toDegrees(extractedYangle) << ' ' <<
toDegrees(extractedZangle) << std::endl;
return 0;
}