Come estrarre gli angoli di eulero dalla matrice di trasformazione?


12

Ho una semplice realizzazione del motore di gioco di entità / componente.
Il componente Trasforma dispone di metodi per impostare la posizione locale, la rotazione locale, la posizione globale e la rotazione globale.

Se la trasformazione viene impostata su una nuova posizione globale, anche la posizione locale cambia, per aggiornare la posizione locale in tal caso sto solo applicando la matrice locale di trasformazione corrente alla matrice mondiale di trasformazione del genitore.

Fino ad allora non ho problemi, posso ottenere la matrice di trasformazione locale aggiornata.
Ma sto lottando su come aggiornare la posizione locale e il valore di rotazione in trasformazione. L'unica soluzione che ho in mente è estrarre i valori di traduzione e rotazione da localMatrix of transform.

Per la traduzione è abbastanza semplice: prendo solo i valori della quarta colonna. ma che dire della rotazione?
Come estrarre gli angoli di eulero dalla matrice di trasformazione?

Tale soluzione è corretta ?:
Per trovare la rotazione attorno all'asse Z, possiamo trovare la differenza tra il vettore dell'asse X di localTransform e il vettore dell'asse X di parent.localTransform e memorizzare il risultato in Delta, quindi: localRotation.z = atan2 (Delta.y, Delta .X);

Lo stesso vale per la rotazione attorno a X e Y, basta scambiare l'asse.

Risposte:


10

Normalmente conservo tutti gli oggetti come matrici 4x4 (potresti fare 3x3 ma per me è più facile avere solo 1 classe) invece di tradurre avanti e indietro tra un set di 4x4 e 3 di vector3s (traslazione, rotazione, scala). Gli angoli di Eulero sono notoriamente difficili da affrontare in alcuni scenari, quindi consiglierei di usare Quaternions se si desidera davvero archiviare i componenti anziché una matrice.

Ma ecco un po 'di codice che ho trovato qualche tempo fa che funziona. Spero che questo aiuti, purtroppo non ho la fonte originale per dove l'ho trovato. Non ho idea di quali scenari dispari potrebbe non funzionare. Attualmente lo sto usando per ottenere la rotazione delle matrici 4x4 ruotate a sinistra di 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);
        }
    }

Ecco un altro thread che ho trovato mentre cercavo di rispondere alla tua domanda che sembrava un risultato simile al mio.

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


Sembra che la mia soluzione proposta sia quasi giusta, ma non so perché non viene utilizzato atan2 asin per il pitch.

Inoltre, come mi aiuterebbe se memorizzassi ciascun componente in mat4x4 separato? Come potrei quindi ottenere, ad esempio, l'angolo di rotazione in uscita attorno ad un asse?

La tua domanda originale mi porta a credere che stai memorizzando i tuoi oggetti come 3 vector3s: traduzione, rotazione e scala. Quindi, quando crei un LocalTransform da quelli che fanno un po 'di lavoro e in seguito tentano di riconvertire (localTransform * globalTransform) in 3 vector3s. Potrei sbagliarmi totalmente, stavo solo ottenendo quell'impressione.
Ntsc Cobalt,

Sì, non conosco abbastanza bene la matematica per il motivo per cui l'intonazione viene eseguita con ASIN, ma la domanda collegata utilizza la stessa matematica, quindi credo che sia corretta. Uso questa funzione da un po 'di tempo senza alcun problema.
Ntsc Cobalt,

C'è qualche motivo particolare per usare atan2f nei primi due casi e atan2 nel terzo, o è un errore di battitura?
Mattias F,

10

C'è un ottimo resoconto su questo processo di Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Ora è anche implementato in glm, a partire dalla versione 0.9.7.0, 02/08/2015. Controlla l'implementazione .

Per capire la matematica, dovresti guardare i valori che sono nella tua matrice di rotazione. Inoltre, devi conoscere l'ordine in cui sono state applicate le rotazioni per creare la tua matrice al fine di estrarre correttamente i valori.

Una matrice di rotazione dagli angoli di Eulero si forma combinando le rotazioni attorno agli assi x, y e z. Ad esempio, con la matrice è possibile ruotare di around gradi attorno a Z.

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

Matrici simili esistono per ruotare attorno agli assi X e Y:

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

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

Possiamo moltiplicare queste matrici insieme per creare una matrice che è il risultato di tutte e tre le rotazioni. È importante notare che l'ordine in cui queste matrici vengono moltiplicate insieme è importante, poiché la moltiplicazione delle matrici non è commutativa . Questo significa che Rx*Ry*Rz ≠ Rz*Ry*Rx. Consideriamo un possibile ordine di rotazione, zyx. Quando le tre matrici vengono combinate, si ottiene una matrice simile a questa:

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

dove Cxè il coseno xdell'angolo di rotazione, Sxè il seno xdell'angolo di rotazione, ecc.

Ora, la sfida è quella di estrarre l'originale x, ye zvalori che è andato in matrice.

Prima di tutto xtiriamo fuori l' angolo. Se conosciamo il sin(x)e cos(x), possiamo usare la funzione tangente inversa atan2per restituirci il nostro angolo. Sfortunatamente, questi valori non compaiono da soli nella nostra matrice. Ma, se diamo uno sguardo più da vicino agli elementi M[1][2]e M[2][2], possiamo vedere che lo sappiamo -sin(x)*cos(y)bene cos(x)*cos(y). Poiché la funzione tangente è il rapporto tra i lati opposti e adiacenti di un triangolo, il ridimensionamento di entrambi i valori della stessa quantità (in questo caso cos(y)) produrrà lo stesso risultato. Così,

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

Ora proviamo a ottenere y. Lo sappiamo sin(y)da M[0][2]. Se avessimo cos (y), potremmo usarlo di atan2nuovo, ma non abbiamo quel valore nella nostra matrice. Tuttavia, a causa dell'identità pitagorica , sappiamo che:

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

Quindi, possiamo calcolare y:

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

Infine, dobbiamo calcolare z. È qui che l'approccio di Mike Day differisce dalla risposta precedente. Poiché a questo punto conosciamo la quantità xe la yrotazione, possiamo costruire una matrice di rotazione XY e trovare la quantità di zrotazione necessaria per abbinare la matrice target. La RxRymatrice si presenta così:

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

Poiché sappiamo che RxRy* Rzè uguale alla nostra matrice di input M, possiamo usare questa matrice per tornare a Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

L' inverso di una matrice di rotazione è la sua trasposizione , quindi possiamo espanderlo 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 

Ora possiamo risolvere per sinZed cosZeseguendo la moltiplicazione della matrice. Dobbiamo solo calcolare gli elementi [1][0]e [1][1].

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

Ecco un'implementazione completa per riferimento:

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

Nota, tuttavia, il problema quando y = pi / 2 e quindi cos (y) == 0. Quindi NON è possibile che M [1] [3] e M [2] [3] possano essere usati per ottenere x perché il rapporto non è definito e non è possibile ottenere un valore atan2 . Credo che ciò equivale al problema del blocco del gimbal .
Pieter Geerkens,

@PieterGeerkens, hai ragione, questo è il gimbal lock. A proposito, il tuo commento ha rivelato che avevo un refuso in quella sezione. Mi riferisco agli indici di matrice con il primo a 0 e poiché sono matrici 3x3, l'ultimo indice è 2, non 3. Ho corretto M[1][3]con M[1][2]e M[2][3]con M[2][2].
Chris,

Sono abbastanza sicuro che la prima colonna della seconda riga della matrice combinata di esempio sia SxSyCz + CxSz, non SxSySz + CxSz!
Lago

@Lake, hai ragione. Modificato.
Chris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.