15
\$\begingroup\$

I have a simple realisation of entity/component game engine.
Transform component have methods to set local position, local rotation, global position and global rotation.

If transform is being set new global position, then local position also changes, to update local position in such case I'm just applying current transform local matrix to parent's transform world matrix.

Until then I have no problems, I can get updated local transform matrix.
But I'm struggling on how to update local position and rotation value in transform. Only solution I have in mind is to extract translation & rotation values from localMatrix of transform.

For translation it's quite easy - I just take 4th column values. but what's about rotation?
How to extract euler angles from transformation matrix?

Is such solution right?:
To find rotation around Z axis, we can find difference between X axis vector of localTransform and X axis vector of parent.localTransform and store result in Delta, then: localRotation.z = atan2(Delta.y, Delta.x);

Same for rotation around X & Y, just need to swap axis.

\$\endgroup\$

3 Answers 3

11
\$\begingroup\$

Normally I store all objects as 4x4 Matrices (you could do 3x3 but easier for me just to have 1 class) instead of translating back and forth between a 4x4 and 3 sets of vector3s (Translation, Rotation, Scale). Euler angles are notoriously difficult to deal with in certain scenarios so I would recommend using Quaternions if you really want to store the components instead of a matrix.

But here is some code I found a while back that works. I hope this helps, unfortunately I do not have the original source for where I found this. I have no idea what odd scenarios it may not work in. I am currently using this to get the rotation of YawPitchRoll rotated, left handed 4x4 matrices.

   union {
        struct 
        {
            float        _11, _12, _13, _14;
            float        _21, _22, _23, _24;
            float        _31, _32, _33, _34;
            float        _41, _42, _43, _44;
        };
        float m[4][4];
        float m2[16];
    };

    inline void GetRotation(float& Yaw, float& Pitch, float& Roll) const
    {
        if (_11 == 1.0f)
        {
            Yaw = atan2f(_13, _34);
            Pitch = 0;
            Roll = 0;

        }else if (_11 == -1.0f)
        {
            Yaw = atan2f(_13, _34);
            Pitch = 0;
            Roll = 0;
        }else 
        {

            Yaw = atan2(-_31,_11);
            Pitch = asin(_21);
            Roll = atan2(-_23,_22);
        }
    }

Here is another thread I found while trying to answer your question that looked like a similar result to mine.

https://stackoverflow.com/questions/1996957/conversion-euler-to-matrix-and-matrix-to-euler

\$\endgroup\$
5
  • \$\begingroup\$ It seems that my proposed solution is almost right, just don't know why isntead of atan2 asin is used for pitch. \$\endgroup\$
    – user27061
    Commented Mar 13, 2013 at 16:57
  • \$\begingroup\$ Also, how it would help me, if I would store each component in separate mat4x4? How could I then get and e.g. output angle of rotation around some axis? \$\endgroup\$
    – user27061
    Commented Mar 13, 2013 at 16:57
  • \$\begingroup\$ Your original question lead me to believe that you are storing your objects as 3 vector3s: Translation, Rotation, and Scale. Then when you creating a localTransform out of those doing some work and later attempting to convert (localTransform*globalTransform) back into 3 vector3s. I could be totally wrong I was just getting that impression. \$\endgroup\$
    – NtscCobalt
    Commented Mar 13, 2013 at 17:00
  • \$\begingroup\$ Yeah I don't know the math well enough for why pitch is done with ASIN but the linked question uses the same math so I believe it is correct. I've been using this function for a while to without any issue. \$\endgroup\$
    – NtscCobalt
    Commented Mar 13, 2013 at 17:02
  • \$\begingroup\$ Is there any particular reason for using atan2f in the first two if cases and atan2 in the third, or is it a typo? \$\endgroup\$
    – Mattias F
    Commented Feb 13, 2017 at 13:16
12
\$\begingroup\$

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;
}
\$\endgroup\$
4
  • \$\begingroup\$ Note, however, the problem when y = pi/2 and thus cos(y) == 0. Then it is NOT the case that M[1][3] and M[2][3] can be used to obtain x because the ratio is undefined, and not atan2 value can be obtained. I believe that this is equivalent to the gimbal lock problem. \$\endgroup\$ Commented Dec 3, 2015 at 23:18
  • \$\begingroup\$ @PieterGeerkens, you're right, that is gimbal lock. BTW, your comment revealed that I had a typo in that section. I refer to the matrix indices with the first one at 0, and since they are 3x3 matrices, the last index is 2, not 3. I've corrected M[1][3] with M[1][2], and M[2][3] with M[2][2]. \$\endgroup\$
    – Chris
    Commented Dec 4, 2015 at 0:17
  • \$\begingroup\$ I'm pretty sure second row first column of the example combined matrix is SxSyCz + CxSz, not SxSySz + CxSz! \$\endgroup\$
    – Lake
    Commented Nov 18, 2016 at 15:39
  • \$\begingroup\$ @Lake, you're correct. Edited. \$\endgroup\$
    – Chris
    Commented Nov 18, 2016 at 18:13
-1
\$\begingroup\$

There's a function in scipy called "scipy.spatial.transform". You can look over in Rotation method which is trasform between euler_angles <-> R-matrix <-> quaternion.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Since the question doesn't say they're using Python, this answer would be better if it explained how to implement a method like this that does not rely on a specific Python library. \$\endgroup\$
    – DMGregory
    Commented May 30, 2020 at 14:38

You must log in to answer this question.