Bagaimana cara mengekstrak sudut euler dari matriks transformasi?

12

Saya memiliki realisasi sederhana dari mesin permainan entitas / komponen.
Transform komponen memiliki metode untuk mengatur posisi lokal, rotasi lokal, posisi global dan rotasi global.

Jika transformasi sedang mengatur posisi global baru, maka posisi lokal juga berubah, untuk memperbarui posisi lokal dalam kasus seperti itu, saya hanya menerapkan matriks transformasi lokal saat ini ke matriks transformasi dunia induk.

Sampai saat itu saya tidak punya masalah, saya bisa mendapatkan matriks transformasi lokal yang diperbarui.
Tetapi saya sedang berjuang bagaimana memperbarui posisi lokal dan nilai rotasi dalam transformasi. Satu-satunya solusi yang ada dalam pikiran saya adalah mengekstraksi nilai terjemahan & rotasi dari localMatrix of transform.

Untuk terjemahannya cukup mudah - saya hanya mengambil nilai kolom ke-4. tapi bagaimana dengan rotasi?
Bagaimana cara mengekstrak sudut euler dari matriks transformasi?

Apakah solusi seperti itu benar ?:
Untuk menemukan rotasi di sekitar sumbu Z, kita dapat menemukan perbedaan antara vektor sumbu X dari localTransform dan vektor sumbu X dari parent.localTransform dan menyimpan hasil di Delta, lalu: localRotation.z = atan2 (Delta.y, Delta .x);

Sama untuk rotasi di sekitar X & Y, hanya perlu menukar sumbu.


sumber

Jawaban:

10

Biasanya saya menyimpan semua objek sebagai Matriks 4x4 (Anda bisa melakukan 3x3 tetapi lebih mudah bagi saya hanya memiliki 1 kelas) daripada menerjemahkan bolak-balik antara 4x4 dan 3 set vector3s (Terjemahan, Rotasi, Skala). Sudut Euler terkenal sulit untuk ditangani dalam skenario tertentu jadi saya akan merekomendasikan menggunakan Quaternions jika Anda benar-benar ingin menyimpan komponen, bukan matriks.

Tapi di sini ada beberapa kode yang saya temukan beberapa waktu lalu yang berfungsi. Saya harap ini membantu, sayangnya saya tidak memiliki sumber asli untuk tempat saya menemukan ini. Saya tidak tahu skenario aneh apa yang mungkin tidak berhasil. Saya saat ini menggunakan ini untuk mendapatkan rotasi YawPitchRoll diputar, matriks 4x4 tangan kiri.

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

Ini adalah utas lain yang saya temukan ketika mencoba menjawab pertanyaan Anda yang terlihat seperti hasil yang serupa dengan saya.

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

NtscCobalt
sumber
Tampaknya solusi yang saya usulkan hampir benar, hanya saja tidak tahu mengapa isntead dari atan2 digunakan untuk pitch.
Juga, bagaimana itu akan membantu saya, jika saya akan menyimpan setiap komponen dalam mat4x4 terpisah? Bagaimana saya bisa mendapatkan dan misalnya sudut keluaran rotasi di sekitar beberapa sumbu?
Pertanyaan awal Anda membuat saya percaya bahwa Anda menyimpan objek Anda sebagai 3 vector3s: Terjemahan, Rotasi, dan Skala. Kemudian ketika Anda membuat localTransform dari mereka yang melakukan beberapa pekerjaan dan kemudian mencoba untuk mengkonversi (localTransform * globalTransform) kembali menjadi 3 vektor3. Saya bisa salah total. Saya hanya mendapatkan kesan itu.
NtscCobalt
Ya saya tidak tahu matematika dengan cukup baik mengapa pitch dilakukan dengan ASIN tetapi pertanyaan terkait menggunakan matematika yang sama jadi saya percaya itu benar. Saya telah menggunakan fungsi ini untuk sementara waktu tanpa masalah.
NtscCobalt
Apakah ada alasan khusus untuk menggunakan atan2f di dua yang pertama jika kasus dan atan2 di yang ketiga, atau apakah itu salah ketik?
Mattias F
10

Ada proses penulisan bagus untuk proses ini oleh Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Itu juga sekarang diimplementasikan di glm, pada versi 0.9.7.0, 02/08/2015. Lihat implementasinya .

Untuk memahami matematika, Anda harus melihat nilai-nilai yang ada dalam matriks rotasi Anda. Selain itu, Anda harus mengetahui urutan rotasi diterapkan untuk membuat matriks Anda agar dapat mengekstrak nilai dengan benar.

Matriks rotasi dari sudut Euler dibentuk dengan menggabungkan rotasi di sekitar sumbu x, y, dan z. Misalnya, memutar θ derajat di sekitar Z dapat dilakukan dengan matriks

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

Matriks serupa ada untuk memutar sumbu X dan Y:

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

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

Kita dapat mengalikan matriks-matriks ini bersama-sama untuk membuat satu matriks yang merupakan hasil dari ketiga rotasi. Penting untuk dicatat bahwa urutan matriks ini dikalikan bersama adalah penting, karena perkalian matriks tidak komutatif . Ini artinya Rx*Ry*Rz ≠ Rz*Ry*Rx. Mari kita pertimbangkan satu kemungkinan urutan rotasi, zyx. Ketika tiga matriks digabungkan, ini menghasilkan matriks yang terlihat seperti ini:

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

dimana Cxcosinus dari xsudut rotasi, Sxadalah sinus darix sudut rotasi, dll.

Sekarang, tantangannya adalah untuk mengekstrak asli x, ydanz nilai-nilai yang masuk ke matriks.

Pertama mari kita xkeluar sudut. Jika kita tahu sin(x)dan cos(x), kita bisa menggunakan fungsi tangen terbalik atan2untuk mengembalikan sudut kita. Sayangnya, nilai-nilai itu tidak muncul sendiri dalam matriks kami. Tapi, jika kita melihat lebih dekat pada unsur-unsur M[1][2]dan M[2][2], kita dapat melihat kita tahu -sin(x)*cos(y)serta cos(x)*cos(y). Karena fungsi tangen adalah rasio sisi yang berlawanan dan berdekatan dari sebuah segitiga, penskalaan kedua nilai dengan jumlah yang sama (dalam hal ini cos(y)) akan menghasilkan hasil yang sama. Jadi,

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

Sekarang mari kita coba y. Kami tahu sin(y)dari M[0][2]. Jika kita memiliki cos (y), kita bisa menggunakan atan2lagi, tetapi kita tidak memiliki nilai itu dalam matriks kita. Namun, karena identitas Pythagoras , kita tahu bahwa:

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

Jadi, kita dapat menghitung y:

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

Terakhir, kita perlu menghitung z. Di sinilah pendekatan Mike Day berbeda dari jawaban sebelumnya. Karena pada titik ini kita mengetahui jumlah xdan yrotasi, kita dapat membangun matriks rotasi XY, dan menemukan jumlah zrotasi yang diperlukan untuk mencocokkan matriks target. The RxRymatriks terlihat seperti ini:

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

Karena kita tahu bahwa RxRy* Rzsama dengan matriks input Mkita, kita dapat menggunakan matriks ini untuk kembali ke Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

The inverse dari matriks rotasi transposnya , sehingga kita dapat memperluas ini untuk:

 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 

Kita sekarang dapat memecahkan untuk sinZdan cosZdengan melakukan perkalian matriks. Kami hanya perlu menghitung elemen [1][0]dan [1][1].

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

Berikut ini adalah implementasi lengkap untuk referensi:

#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
sumber
Perhatikan, bagaimanapun, masalah ketika y = pi / 2 dan karenanya cos (y) == 0. Maka BUKAN halnya bahwa M [1] [3] dan M [2] [3] dapat digunakan untuk mendapatkan x karena rasionya tidak terdefinisi, dan nilai atan2 tidak bisa didapatkan. Saya percaya bahwa ini setara dengan masalah kunci gimbal .
Pieter Geerkens
@PieterGeerkens, Anda benar, itu adalah kunci gimbal. BTW, komentar Anda mengungkapkan bahwa saya salah ketik di bagian itu. Saya merujuk pada indeks matriks dengan yang pertama di 0, dan karena mereka adalah matriks 3x3, indeks terakhir adalah 2, bukan 3. Saya sudah mengoreksi M[1][3]dengan M[1][2], dan M[2][3]dengan M[2][2].
Chris
Saya cukup yakin baris kedua kolom pertama dari contoh matriks gabungan adalah SxSyCz + CxSz, bukan SxSySz + CxSz!
Danau
@Lake, kamu benar. Diedit.
Chris