Wie extrahiere ich Eulerwinkel aus der Transformationsmatrix?

11

Ich habe eine einfache Realisierung der Entity / Component Game Engine.
Transformationskomponenten verfügen über Methoden zum Festlegen der lokalen Position, der lokalen Rotation, der globalen Position und der globalen Rotation.

Wenn für die Transformation eine neue globale Position festgelegt wird, ändert sich auch die lokale Position, um die lokale Position zu aktualisieren. In diesem Fall wende ich nur die aktuelle lokale Matrix der Transformation auf die Transformationsweltmatrix der Eltern an.

Bis dahin habe ich keine Probleme, ich kann die lokale Transformationsmatrix aktualisieren.
Aber ich habe Probleme damit, die lokale Position und den Rotationswert in der Transformation zu aktualisieren. Die einzige Lösung, die ich im Sinn habe, besteht darin, Translations- und Rotationswerte aus localMatrix of transform zu extrahieren.

Für die Übersetzung ist es ganz einfach - ich nehme nur Werte für die 4. Spalte. aber was ist mit Rotation?
Wie extrahiere ich Eulerwinkel aus der Transformationsmatrix?

Ist eine solche Lösung richtig?:
Um die Drehung um die Z-Achse zu ermitteln, können wir den Unterschied zwischen dem X-Achsenvektor von localTransform und dem X-Achsenvektor von parent.localTransform ermitteln und das Ergebnis in Delta speichern. Dann gilt Folgendes: localRotation.z = atan2 (Delta.y, Delta .x);

Gleiches gilt für die Drehung um X & Y, Sie müssen nur die Achse tauschen.


quelle

Antworten:

9

Normalerweise speichere ich alle Objekte als 4x4-Matrizen (Sie könnten 3x3 machen, aber es ist einfacher für mich, nur 1 Klasse zu haben), anstatt zwischen einem 4x4- und 3-Satz von Vektoren3s (Translation, Rotation, Scale) hin und her zu übersetzen. Euler-Winkel sind in bestimmten Szenarien notorisch schwierig zu behandeln, daher würde ich die Verwendung von Quaternionen empfehlen, wenn Sie die Komponenten wirklich anstelle einer Matrix speichern möchten.

Aber hier ist ein Code, den ich vor einiger Zeit gefunden habe und der funktioniert. Ich hoffe das hilft, leider habe ich nicht die Originalquelle für wo ich das gefunden habe. Ich habe keine Ahnung, in welchen seltsamen Szenarien es möglicherweise nicht funktioniert. Ich verwende dies derzeit, um die Rotation von YawPitchRoll-gedrehten 4x4-Matrizen für Linkshänder zu erhalten.

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

Hier ist ein weiterer Thread, den ich gefunden habe, als ich versucht habe, Ihre Frage zu beantworten, die wie ein ähnliches Ergebnis wie meine aussah.

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

NtscCobalt
quelle
Es scheint, dass meine vorgeschlagene Lösung fast richtig ist, ich weiß nur nicht, warum nicht atan2 asin für die Tonhöhe verwendet wird.
Wie würde es mir auch helfen, wenn ich jede Komponente in einem separaten mat4x4 speichern würde? Wie könnte ich dann einen Drehwinkel um eine Achse bekommen und zB ausgeben?
Ihre ursprüngliche Frage hat mich zu der Annahme geführt, dass Sie Ihre Objekte als drei Vektoren speichern: Übersetzung, Drehung und Skalierung. Wenn Sie dann eine localTransform aus denjenigen erstellen, die etwas arbeiten, und später versuchen, (localTransform * globalTransform) wieder in 3 vector3s zu konvertieren. Ich könnte völlig falsch liegen, ich hatte gerade diesen Eindruck.
NtscCobalt
Ja, ich kenne die Mathematik nicht gut genug, um zu verstehen, warum die Tonhöhe mit ASIN gemacht wird, aber die verknüpfte Frage verwendet dieselbe Mathematik, daher glaube ich, dass sie korrekt ist. Ich benutze diese Funktion seit einiger Zeit ohne Probleme.
NtscCobalt
Gibt es einen bestimmten Grund für die Verwendung von atan2f in den ersten beiden Fällen und atan2 in den dritten Fällen oder handelt es sich um einen Tippfehler?
Mattias F
9

Es gibt eine großartige Zusammenfassung zu diesem Prozess von Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Es ist jetzt auch in glm ab Version 0.9.7.0 vom 02.08.2015 implementiert. Überprüfen Sie die Implementierung .

Um die Mathematik zu verstehen, sollten Sie sich die Werte in Ihrer Rotationsmatrix ansehen. Außerdem müssen Sie die Reihenfolge kennen, in der die Rotationen angewendet wurden, um Ihre Matrix zu erstellen, damit die Werte ordnungsgemäß extrahiert werden können.

Eine Rotationsmatrix aus Eulerwinkeln wird durch Kombinieren von Rotationen um die x-, y- und z-Achse gebildet. Zum Beispiel kann mit der Matrix eine Drehung von θ Grad um Z durchgeführt werden

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

Ähnliche Matrizen existieren für die Drehung um die X- und Y-Achse:

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

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

Wir können diese Matrizen miteinander multiplizieren, um eine Matrix zu erstellen, die das Ergebnis aller drei Rotationen ist. Es ist wichtig zu beachten, dass die Reihenfolge, in der diese Matrizen miteinander multipliziert werden, wichtig ist, da die Matrixmultiplikation nicht kommutativ ist . Das bedeutet das Rx*Ry*Rz ≠ Rz*Ry*Rx. Betrachten wir eine mögliche Rotationsreihenfolge, zyx. Wenn die drei Matrizen kombiniert werden, ergibt sich eine Matrix, die folgendermaßen aussieht:

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

Wo Cxist der Cosinus des xDrehwinkels, Sxist der Sinus desx Drehwinkels usw.

Nun besteht die Herausforderung darin , das Original zu extrahieren x, yundz Werte , die in die Matrix ging.

Lassen Sie uns zuerst den xWinkel herausholen. Wenn wir das sin(x)und kennen cos(x), können wir die inverse Tangentenfunktion verwenden atan2, um unseren Winkel zurückzugeben. Leider erscheinen diese Werte nicht von alleine in unserer Matrix. Aber wenn wir uns die Elemente genauer ansehen M[1][2]und sehen M[2][2], dass wir es -sin(x)*cos(y)genauso gut wissen wie cos(x)*cos(y). Da die Tangentenfunktion das Verhältnis der gegenüberliegenden und benachbarten Seiten eines Dreiecks ist, führt die Skalierung beider Werte um den gleichen Betrag (in diesem Fall cos(y)) zum gleichen Ergebnis. Somit,

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

Jetzt lass uns versuchen zu bekommen y. Wir wissen sin(y)von M[0][2]. Wenn wir cos (y) hätten, könnten wir es atan2wieder verwenden, aber wir haben diesen Wert nicht in unserer Matrix. Jedoch aufgrund der pythagoreischen Identität , wir wissen , dass:

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

So können wir berechnen y:

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

Zuletzt müssen wir berechnen z. Hier unterscheidet sich der Ansatz von Mike Day von der vorherigen Antwort. Da wir zu diesem Zeitpunkt den Betrag xund die yRotation kennen, können wir eine XY-Rotationsmatrix erstellen und den zRotationsbetrag ermitteln, der zur Anpassung an die Zielmatrix erforderlich ist. Die RxRyMatrix sieht folgendermaßen aus:

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

Da wir wissen, dass RxRy* Rzunserer Eingabematrix entspricht M, können wir diese Matrix verwenden, um zu Folgendem zurückzukehren Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

Die Umkehrung einer Rotationsmatrix ist ihre Transponierung , daher können wir diese erweitern auf:

 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 

Wir können jetzt nach sinZund cosZdurch Ausführen der Matrixmultiplikation lösen . Wir müssen nur die Elemente [1][0]und berechnen [1][1].

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

Hier ist eine vollständige Implementierung als Referenz:

#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
quelle
Man beachte jedoch das Problem, wenn y = pi / 2 und damit cos (y) == 0. Dann ist es NICHT der Fall, dass M [1] [3] und M [2] [3] verwendet werden können, um x zu erhalten weil das Verhältnis undefiniert ist und kein atan2- Wert erhalten werden kann. Ich glaube, dass dies dem Problem der Kardanverriegelung entspricht .
Pieter Geerkens
@PieterGeerkens, du hast recht, das ist Gimbal Lock. Übrigens, Ihr Kommentar ergab, dass ich in diesem Abschnitt einen Tippfehler hatte. Ich beziehe mich auf die Matrixindizes mit dem ersten bei 0, und da es sich um 3x3-Matrizen handelt, ist der letzte Index 2, nicht 3. Ich habe M[1][3]mit M[1][2]und M[2][3]mit korrigiert M[2][2].
Chris
Ich bin mir ziemlich sicher, dass die erste Spalte der zweiten Zeile der kombinierten Beispielmatrix SxSyCz + CxSz ist, nicht SxSySz + CxSz!
See
@ Lake, du bist richtig. Bearbeitet.
Chris