Jak wyodrębnić kąty eulera z macierzy transformacji?

12

Mam prostą realizację silnika gry encja / komponent.
Komponent Transform ma metody ustalania pozycji lokalnej, rotacji lokalnej, pozycji globalnej i rotacji globalnej.

Jeśli transformacja jest ustalana jako nowa pozycja globalna, wówczas zmienia się także pozycja lokalna, aby zaktualizować pozycję lokalną w takim przypadku po prostu stosuję bieżącą lokalną macierz transformacji do macierzy świata transformacji rodzica.

Do tego czasu nie mam problemów, mogę uzyskać zaktualizowaną macierz transformacji lokalnej.
Ale walczę o to, jak zaktualizować lokalną wartość położenia i obrotu w transformacji. Jedynym rozwiązaniem, które mam na myśli, jest wydobycie wartości translacji i rotacji z localMatrix transformacji.

Do tłumaczenia jest to dość łatwe - po prostu biorę wartości z 4 kolumny. ale co z rotacją?
Jak wyodrębnić kąty eulera z macierzy transformacji?

Czy takie rozwiązanie jest właściwe ?:
Aby znaleźć obrót wokół osi Z, możemy znaleźć różnicę między wektorem osi X localTransform i wektorem osi X rodzica.localTransform i zapisać wynik w Delta, a następnie: localRotation.z = atan2 (Delta.y, Delta .x);

To samo dotyczy obrotu wokół X i Y, wystarczy zamienić oś.


źródło

Odpowiedzi:

10

Zwykle przechowuję wszystkie obiekty jako macierze 4x4 (możesz zrobić 3x3, ale dla mnie łatwiej jest mieć tylko 1 klasę) zamiast tłumaczyć w przód iw tył między 4x4 i 3 zestawami vector3s (Translacja, Obrót, Skala). Kąty Eulera są bardzo trudne do radzenia sobie w niektórych scenariuszach, więc polecam użycie Quaternions, jeśli naprawdę chcesz przechowywać komponenty zamiast macierzy.

Ale oto kod, który znalazłem jakiś czas temu, który działa. Mam nadzieję, że to pomaga, niestety nie mam oryginalnego źródła, z którego to znalazłem. Nie mam pojęcia, w jakich dziwnych scenariuszach może to nie działać. Obecnie używam tego, aby uzyskać obrót YawPitchRoll obróconych, leworęcznych matryc 4x4.

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

Oto kolejny wątek, który znalazłem, próbując odpowiedzieć na twoje pytanie, który wyglądał podobnie do mojego.

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

NtscCobalt
źródło
Wydaje się, że moje zaproponowane rozwiązanie jest prawie właściwe, po prostu nie wiem, dlaczego isntead of atan2 asin jest używany do pitch.
Ponadto, w jaki sposób pomogłoby mi to, gdybym przechował każdy komponent w osobnym mat4x4? Jak mogę następnie uzyskać i np. Wyjściowy kąt obrotu wokół jakiejś osi?
Twoje oryginalne pytanie doprowadziło mnie do wniosku, że przechowujesz swoje obiekty jako 3 wektory 3: Tłumaczenie, Obrót i Skalę. Następnie, gdy tworzysz localTransform z tych, którzy wykonują jakąś pracę, a następnie próbujesz przekonwertować (localTransform * globalTransform) z powrotem na 3 wektory3. Mogłem się całkowicie mylić, właśnie miałem takie wrażenie.
NtscCobalt
Tak, nie znam matematyki wystarczająco dobrze, aby wyjaśnić, dlaczego pitch jest wykonywany za pomocą ASIN, ale powiązane pytanie używa tej samej matematyki, więc uważam, że jest poprawna. Korzystam z tej funkcji przez jakiś czas bez żadnych problemów.
NtscCobalt
Czy jest jakiś konkretny powód, aby używać atan2f w pierwszych dwóch przypadkach i atan2 w trzecim, czy też jest to literówka?
Mattias F
10

Mike Day ma świetny opis tego procesu: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Jest również teraz zaimplementowany w glm, począwszy od wersji 0.9.7.0, 02/08/2015. Sprawdź wdrożenie .

Aby zrozumieć matematykę, powinieneś przyjrzeć się wartościom, które znajdują się w macierzy rotacji. Ponadto musisz znać kolejność stosowania rotacji, aby utworzyć matrycę w celu prawidłowego wyodrębnienia wartości.

Macierz obrotu z kątów Eulera powstaje przez połączenie obrotów wokół osi x, y i z. Na przykład obracanie o θ stopni wokół Z można wykonać za pomocą macierzy

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

Istnieją podobne macierze do obracania się wokół osi X i Y:

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

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

Możemy pomnożyć te macierze razem, aby utworzyć jedną macierz, która jest wynikiem wszystkich trzech rotacji. Należy zauważyć, że kolejność mnożenia tych macierzy jest ważna, ponieważ mnożenie macierzy nie jest przemienne . To znaczy że Rx*Ry*Rz ≠ Rz*Ry*Rx. Rozważmy jeden możliwy porządek obrotu, zyx. Po połączeniu trzech macierzy powstaje macierz, która wygląda następująco:

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

gdzie Cxjest cosinusx kąta obrotu, Sxsinus xkąta obrotu itp.

Teraz wyzwaniem jest wyodrębnienie oryginału x , yoraz zwartości, które poszedł do matrycy.

Najpierw ustalmy xkąt. Jeśli znamy sin(x)i cos(x), możemy użyć funkcji odwrotnej stycznej, atan2aby przywrócić nam kąt. Niestety wartości te nie pojawiają się same w naszej matrycy. Ale jeśli przyjrzymy się bliżej elementom M[1][2]i M[2][2]zobaczymy, że wiemy -sin(x)*cos(y)równie dobrze cos(x)*cos(y). Ponieważ funkcja styczna jest stosunkiem przeciwnych i sąsiednich boków trójkąta, skalowanie obu wartości o tę samą wartość (w tym przypadku cos(y)) da ten sam wynik. A zatem,

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

Teraz spróbujmy zdobyć y. Wiemy sin(y)z M[0][2]. Gdybyśmy mieli cos (y), moglibyśmy użyć atan2ponownie, ale nie mamy tej wartości w naszej macierzy. Jednak ze względu na tożsamość pitagorejską wiemy, że:

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

Możemy więc obliczyć y :

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

Na koniec musimy obliczyć z. Tutaj podejście Mike'a Day różni się od poprzedniej odpowiedzi. Ponieważ w tym momencie znamy wielkość xi yobrót, możemy zbudować macierz obrotu XY i znaleźć wielkość zobrotu niezbędną do dopasowania do macierzy docelowej. RxRyMatryca wygląda tak:

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

Ponieważ wiemy, że RxRy* Rzjest równe naszej macierzy wejściowej M, możemy użyć tej macierzy, aby wrócić doRz :

M = RxRy * Rz

inverse(RxRy) * M = Rz

Odwrotność macierzy rotacji jest jego transpozycji , dzięki czemu mogą się rozszerzyć do tego:

 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 

Możemy teraz rozwiązać dla sinZi cosZprzez mnożenie macierzy. Musimy tylko obliczyć elementy [1][0]i[1][1] .

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

Oto pełna implementacja w celach informacyjnych:

#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
źródło
Należy jednak zwrócić uwagę na problem, gdy y = pi / 2, a zatem cos (y) == 0. W takim przypadku NIE jest możliwe, że M [1] [3] i M [2] [3] można użyć do uzyskania x ponieważ stosunek jest niezdefiniowany i nie można uzyskać wartości atan2 . Uważam, że jest to równoważne z problemem blokady gimbala .
Pieter Geerkens,
@PieterGeerkens, masz rację, to jest blokada gimbala. BTW, twój komentarz ujawnił, że miałem literówkę w tej sekcji. Odnoszę się do wskaźników macierzy z pierwszym o wartości 0, a ponieważ są to macierze 3x3, ostatni indeks to 2, a nie 3. Poprawiłem za M[1][3]pomocą M[1][2]i za M[2][3]pomocą M[2][2].
Chris
Jestem prawie pewien, że drugi wiersz w pierwszym wierszu przykładowej połączonej macierzy to SxSyCz + CxSz, a nie SxSySz + CxSz!
Jezioro
@Lake, masz rację. Edytowane.
Chris