¿Cómo extraer los ángulos de Euler de la matriz de transformación?

12

Tengo una comprensión simple del motor de juego de entidad / componente.
El componente de transformación tiene métodos para establecer la posición local, la rotación local, la posición global y la rotación global.

Si transform se establece en una nueva posición global, entonces la posición local también cambia, para actualizar la posición local en ese caso, solo estoy aplicando la matriz local de transformación actual a la matriz mundial de transformación de los padres.

Hasta entonces no tengo problemas, puedo obtener una matriz de transformación local actualizada.
Pero estoy luchando sobre cómo actualizar la posición local y el valor de rotación en la transformación. La única solución que tengo en mente es extraer los valores de traducción y rotación de la Matriz local de transformación.

Para la traducción es bastante fácil: solo tomo los valores de la cuarta columna. ¿Pero qué hay de la rotación?
¿Cómo extraer los ángulos de Euler de la matriz de transformación?

¿Es correcta esta solución ?:
Para encontrar la rotación alrededor del eje Z, podemos encontrar la diferencia entre el vector del eje X de localTransform y el vector del eje X de parent.localTransform y almacenar el resultado en Delta, luego: localRotation.z = atan2 (Delta.y, Delta .X);

Lo mismo para la rotación alrededor de X e Y, solo necesita cambiar el eje.


fuente

Respuestas:

10

Normalmente almaceno todos los objetos como Matrices 4x4 (podrías hacer 3x3 pero es más fácil para mí solo tener 1 clase) en lugar de traducir de ida y vuelta entre un 4x4 y 3 conjuntos de vectores3 (Traducción, Rotación, Escala). Los ángulos de Euler son notoriamente difíciles de manejar en ciertos escenarios, por lo que recomendaría usar Quaternions si realmente desea almacenar los componentes en lugar de una matriz.

Pero aquí hay un código que encontré hace un tiempo que funciona. Espero que esto ayude, desafortunadamente no tengo la fuente original de dónde encontré esto. No tengo idea de en qué escenarios extraños puede no funcionar. Actualmente estoy usando esto para obtener la rotación de YawPitchRoll rotado, matrices 4x4 zurdas.

   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);
        }
    }

Aquí hay otro hilo que encontré al intentar responder a su pregunta que parecía un resultado similar al mío.

/programming/1996957/conversion-euler-to-matrix-and-matrix-to-euler

NtscCobalt
fuente
Parece que mi solución propuesta es casi correcta, solo que no sé por qué se usa istanad de atan2 asin para pitch.
Además, ¿cómo me ayudaría si almacenara cada componente en un mat4x4 separado? ¿Cómo podría obtener y, por ejemplo, un ángulo de rotación de salida alrededor de algún eje?
Su pregunta original me lleva a creer que está almacenando sus objetos como 3 vectores3: Traducción, Rotación y Escala. Luego, cuando crea un LocalTransform a partir de aquellos que realizan algún trabajo y luego intentan convertir (localTransform * globalTransform) nuevamente en 3 vector3s. Podría estar totalmente equivocado, solo estaba teniendo esa impresión.
NtscCobalt
Sí, no conozco las matemáticas lo suficiente para saber por qué el tono se hace con ASIN, pero la pregunta vinculada usa las mismas matemáticas, así que creo que es correcto. He estado usando esta función durante un tiempo sin ningún problema.
NtscCobalt
¿Hay alguna razón particular para usar atan2f en los primeros dos casos y atan2 en el tercero, o es un error tipográfico?
Mattias F
10

Hay un gran comentario sobre este proceso por Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Ahora también se implementa en glm, a partir de la versión 0.9.7.0, 08/02/2015. Echa un vistazo a la implementación .

Para comprender las matemáticas, debe mirar los valores que están en su matriz de rotación. Además, debe conocer el orden en el que se aplicaron las rotaciones para crear su matriz y extraer los valores correctamente.

Se forma una matriz de rotación desde los ángulos de Euler combinando rotaciones alrededor de los ejes x, y y z. Por ejemplo, la rotación de θ grados alrededor de Z se puede hacer con la matriz

      cosθ  -sinθ   0 
Rz =  sinθ   cosθ   0 
        0      0    1 

Existen matrices similares para rotar alrededor de los ejes X e Y:

       1    0     0   
Rx =   0  cosθ  -sinθ 
       0  sinθ   cosθ 

       cosθ  0   sinθ 
Ry =    0    1    0   
      -sinθ  0   cosθ 

Podemos multiplicar estas matrices para crear una matriz que sea el resultado de las tres rotaciones. Es importante tener en cuenta que el orden en que estas matrices se multiplican juntas es importante, porque la multiplicación de matrices no es conmutativa . Esto significa que Rx*Ry*Rz ≠ Rz*Ry*Rx. Consideremos un posible orden de rotación, zyx. Cuando se combinan las tres matrices, se obtiene una matriz similar a esta:

               CyCz              -CySz        Sy  
RxRyRz =   SxSyCz + CxSz   -SxSySz + CxCz   -SxCy 
          -CxSyCz + SxSz    CxSySz + SxCz    CxCy 

donde Cxestá el coseno del xángulo de rotación, Sxes el seno del xángulo de rotación, etc.

Ahora, el reto consiste en extraer el original x, yy zlos valores que entraron en la matriz.

Primero saquemos el xángulo. Si conocemos el sin(x)y cos(x), podemos usar la función de tangente inversa atan2para devolvernos nuestro ángulo. Desafortunadamente, esos valores no aparecen por sí mismos en nuestra matriz. Pero, si miramos más de cerca los elementos M[1][2]y M[2][2], podemos ver que sabemos -sin(x)*cos(y)tan bien como cos(x)*cos(y). Dado que la función tangente es la razón de los lados opuestos y adyacentes de un triángulo, al escalar ambos valores en la misma cantidad (en este caso cos(y)) se obtendrá el mismo resultado. Así,

x = atan2(-M[1][2], M[2][2])

Ahora intentemos llegar y. Lo sabemos sin(y)por M[0][2]. Si tuviéramos cos (y), podríamos usarlo atan2nuevamente, pero no tenemos ese valor en nuestra matriz. Sin embargo, debido a la identidad pitagórica , sabemos que:

cosY = sqrt(1 - M[0][2])

Entonces, podemos calcular y:

y = atan2(M[0][2], cosY)

Por último, necesitamos calcular z. Aquí es donde el enfoque de Mike Day difiere de la respuesta anterior. Como en este punto sabemos la cantidad xy la yrotación, podemos construir una matriz de rotación XY y encontrar la cantidad de zrotación necesaria para que coincida con la matriz objetivo. La RxRymatriz se ve así:

          Cy     0     Sy  
RxRy =   SxSy   Cx   -SxCy 
        -CxSy   Sx    CxCy 

Como sabemos que RxRy* Rzes igual a nuestra matriz de entrada M, podemos usar esta matriz para volver a Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

La inversa de una matriz de rotación es su transposición , por lo que podemos expandir esto a:

 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 

Ahora podemos resolver sinZy cosZrealizar la multiplicación de matrices. Solo necesitamos calcular los elementos [1][0]y [1][1].

sinZ = cosX * M[1][0] + sinX * M[2][0]
cosZ = coxX * M[1][1] + sinX * M[2][1]
z = atan2(sinZ, cosZ)

Aquí hay una implementación completa para referencia:

#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;
}
Chris
fuente
Sin embargo, tenga en cuenta el problema cuando y = pi / 2 y, por lo tanto, cos (y) == 0. Entonces NO es el caso de que M [1] [3] y M [2] [3] puedan usarse para obtener x porque la relación no está definida y no se puede obtener el valor atan2 . Creo que esto es equivalente al problema del bloqueo del cardán .
Pieter Geerkens
@PieterGeerkens, tienes razón, eso es bloqueo de cardán. Por cierto, su comentario reveló que tenía un error tipográfico en esa sección. Me refiero a los índices de la matriz con el primero en 0, y dado que son matrices de 3x3, el último índice es 2, no 3. He corregido M[1][3]con M[1][2]y M[2][3]con M[2][2].
Chris
¡Estoy bastante seguro de que la primera columna de la segunda fila de la matriz combinada de ejemplo es SxSyCz + CxSz, no SxSySz + CxSz!
Lago
@Lake, tienes razón. Editado
Chris