Comment extraire les angles d'Euler de la matrice de transformation?

12

J'ai une réalisation simple de moteur de jeu entité / composant.
Le composant Transform a des méthodes pour définir la position locale, la rotation locale, la position globale et la rotation globale.

Si transform est en train de définir une nouvelle position globale, la position locale change également, pour mettre à jour la position locale dans ce cas, j'applique simplement la matrice locale de transformation actuelle à la matrice du monde de transformation du parent.

Jusque-là, je n'ai aucun problème, je peux obtenir une matrice de transformation locale mise à jour.
Mais j'ai du mal à mettre à jour la position locale et la valeur de rotation dans la transformation. La seule solution que j'ai en tête est d'extraire les valeurs de translation et de rotation de la matrice locale de transformation.

Pour la traduction, c'est assez facile - je prends juste les valeurs de la 4ème colonne. mais qu'en est-il de la rotation?
Comment extraire les angles d'Euler de la matrice de transformation?

Une telle solution est-elle correcte?:
Pour trouver la rotation autour de l'axe Z, nous pouvons trouver la différence entre le vecteur de l'axe X de localTransform et le vecteur de l'axe X de parent.localTransform et stocker le résultat dans Delta, puis: localRotation.z = atan2 (Delta.y, Delta .X);

Idem pour la rotation autour de X & Y, il suffit de permuter l'axe.


la source

Réponses:

10

Normalement, je stocke tous les objets sous forme de matrices 4x4 (vous pourriez faire 3x3 mais plus facile pour moi juste d'avoir 1 classe) au lieu de faire des allers-retours entre un 4x4 et 3 ensembles de vector3s (Translation, Rotation, Scale). Les angles d'Euler sont notoirement difficiles à gérer dans certains scénarios, donc je recommanderais d'utiliser des Quaternions si vous voulez vraiment stocker les composants au lieu d'une matrice.

Mais voici un code que j'ai trouvé il y a quelque temps et qui fonctionne. J'espère que cela aide, malheureusement je n'ai pas la source d'origine pour savoir où j'ai trouvé cela. Je n'ai aucune idée des scénarios étranges dans lesquels cela peut ne pas fonctionner. J'utilise actuellement cela pour obtenir la rotation des matrices 4x4 pour gaucher YawPitchRoll.

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

Voici un autre fil que j'ai trouvé en essayant de répondre à votre question qui ressemblait à un résultat similaire au mien.

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

NtscCobalt
la source
Il semble que la solution que je propose est presque la bonne, je ne sais pas pourquoi on n'a pas utilisé atan2 asin pour le pitch.
En outre, en quoi cela pourrait-il m'aider si je stockais chaque composant dans un mat4x4 distinct? Comment pourrais-je alors obtenir et par exemple l'angle de rotation de sortie autour d'un axe?
Votre question initiale m'a amené à croire que vous stockez vos objets sous forme de 3 vecteurs3: traduction, rotation et échelle. Ensuite, lorsque vous créez un localTransform à partir de ceux qui font un peu de travail et tentez plus tard de reconvertir (localTransform * globalTransform) en 3 vecteurs3. Je peux me tromper totalement, je viens d'avoir cette impression.
NtscCobalt,
Oui, je ne connais pas assez bien les mathématiques pour savoir pourquoi le pitch est fait avec ASIN, mais la question liée utilise les mêmes mathématiques, donc je pense que c'est correct. J'utilise cette fonction depuis un certain temps sans aucun problème.
NtscCobalt
Y a-t-il une raison particulière d'utiliser atan2f dans les deux premiers cas et atan2 dans le troisième, ou s'agit-il d'une faute de frappe?
Mattias F
10

Il y a une grande synthèse sur ce processus par Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Il est également désormais implémenté dans glm, à partir de la version 0.9.7.0, 02/08/2015. Découvrez l'implémentation .

Pour comprendre les mathématiques, vous devez regarder les valeurs qui se trouvent dans votre matrice de rotation. De plus, vous devez connaître l'ordre dans lequel les rotations ont été appliquées pour créer votre matrice afin d'extraire correctement les valeurs.

Une matrice de rotation à partir des angles d'Euler est formée en combinant des rotations autour des axes x, y et z. Par exemple, la rotation de θ degrés autour de Z peut être effectuée avec la matrice

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

Des matrices similaires existent pour la rotation autour des axes X et Y:

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

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

Nous pouvons multiplier ces matrices ensemble pour créer une matrice qui est le résultat des trois rotations. Il est important de noter que l'ordre de multiplication de ces matrices est important, car la multiplication matricielle n'est pas commutative . Cela veut dire que Rx*Ry*Rz ≠ Rz*Ry*Rx. Considérons un ordre de rotation possible, zyx. Lorsque les trois matrices sont combinées, il en résulte une matrice qui ressemble à ceci:

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

Cxest le cosinus de l' xangle de rotation, Sxest le sinus de l' xangle de rotation, etc.

Maintenant, le défi est d'extraire l'original x, yet les zvaleurs qui sont entrées dans la matrice.

Voyons d'abord l' xangle. Si nous connaissons le sin(x)et cos(x), nous pouvons utiliser la fonction tangente inverse atan2pour nous rendre notre angle. Malheureusement, ces valeurs n'apparaissent pas seules dans notre matrice. Mais, si nous regardons de plus près les éléments M[1][2]et M[2][2], nous pouvons voir que nous le savons -sin(x)*cos(y)aussi cos(x)*cos(y). Étant donné que la fonction tangente est le rapport des côtés opposés et adjacents d'un triangle, la mise à l'échelle des deux valeurs de la même quantité (dans ce cas cos(y)) donnera le même résultat. Donc,

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

Essayons maintenant d'obtenir y. Nous savons sin(y)de M[0][2]. Si nous avions cos (y), nous pourrions utiliser à atan2nouveau, mais nous n'avons pas cette valeur dans notre matrice. Cependant, en raison de l'identité pythagoricienne , nous savons que:

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

Ainsi, nous pouvons calculer y:

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

Enfin, nous devons calculer z. C'est là que l'approche de Mike Day diffère de la réponse précédente. Puisqu'à ce stade, nous connaissons la quantité xet la yrotation, nous pouvons construire une matrice de rotation XY et trouver la quantité de zrotation nécessaire pour correspondre à la matrice cible. La RxRymatrice ressemble à ceci:

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

Puisque nous savons que RxRy* Rzest égal à notre matrice d'entrée M, nous pouvons utiliser cette matrice pour revenir à Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

L' inverse d'une matrice de rotation est sa transposition , nous pouvons donc l'étendre à:

 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 

Nous pouvons maintenant résoudre pour sinZet cosZen effectuant la multiplication matricielle. Il suffit de calculer les éléments [1][0]et [1][1].

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

Voici une implémentation complète pour référence:

#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
la source
Notez cependant le problème lorsque y = pi / 2 et donc cos (y) == 0. Alors, il N'EST PAS possible que M [1] [3] et M [2] [3] puissent être utilisés pour obtenir x car le rapport n'est pas défini et aucune valeur atan2 ne peut être obtenue. Je crois que cela équivaut au problème de verrouillage du cardan .
Pieter Geerkens
@PieterGeerkens, vous avez raison, c'est le verrouillage du cardan. BTW, votre commentaire a révélé que j'avais une faute de frappe dans cette section. Je me réfère aux indices matriciels avec le premier à 0, et comme ce sont des matrices 3x3, le dernier indice est 2, pas 3. J'ai corrigé M[1][3]avec M[1][2]et M[2][3]avec M[2][2].
Chris
Je suis sûr que la deuxième ligne de la première colonne de l'exemple de matrice combinée est SxSyCz + CxSz, pas SxSySz + CxSz!
Lac
@Lake, tu as raison. Édité.
Chris