Як витягти кути ейлера з матриці перетворення?


12

У мене проста реалізація двигуна сутності / компонентів гри.
Компонент трансформації має методи встановлення місцевого положення, локальної ротації, глобальної позиції та глобальної ротації.

Якщо для перетворення встановлюється нове глобальне положення, то місцеве положення також змінюється, щоб оновити локальну позицію в такому випадку, я просто застосовую поточну локальну матрицю перетворення до матриці світового перетворення батьків.

До цього часу у мене немає проблем, я можу отримати оновлену матрицю локальних перетворень.
Але я бореться над тим, як оновити місцеве значення і значення обертання при перетворенні. Єдине рішення, яке я маю на увазі, - це отримання значень перекладу та обертання з localMatrix перетворення.

Для перекладу це досить просто - я просто беру значення 4-го стовпця. а як же обертання?
Як витягти кути ейлера з матриці перетворення?

Чи правильно таке рішення?:
Щоб знайти обертання навколо осі Z, ми можемо знайти різницю між вектором осі X локальної трансформації та вектором осі X батьківського.localTransform та результатом збереження в Delta, тоді: localRotation.z = atan2 (Delta.y, Delta .x);

Те ж саме для обертання навколо X & Y, просто потрібно поміняти вісь.

Відповіді:


10

Зазвичай я зберігаю всі об'єкти як матриці 4x4 (ви можете зробити 3x3, але простіше мені просто мати 1 клас), а не перекладати туди-назад між 4x4 та 3 наборами vector3s (Переклад, Поворот, Шкала). Кути Ейлера, як відомо, важко вирішити в певних сценаріях, тому я рекомендую використовувати Quaternions, якщо ви дійсно хочете зберігати компоненти замість матриці.

Але ось якийсь код, який я знайшов у минулому, який працює. Я сподіваюся, що це допомагає, на жаль, у мене немає оригінального джерела, де я це знайшов. Я поняття не маю, в яких дивних сценаріях це може не працювати. Зараз я використовую це для обертання обертається YawPitchRoll, ліворуч матриць 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);
        }
    }

Ось ще одна тема, яку я знайшов, намагаючись відповісти на ваше запитання, схожий на мій результат.

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


Здається, що моє запропоноване рішення майже вірно, просто не знаю, чому isntead of atan2 asin використовується для тону.

Крім того, як мені це допоможе, якщо я зберігатиму кожен компонент в окремому mat4x4? Як я міг тоді отримати, наприклад, вихідний кут обертання навколо якоїсь осі?

Ваше первісне запитання спонукає мене до думки, що ви зберігаєте свої об'єкти як 3 вектори3: Переклад, Обертання та Масштаб. Потім, коли ви створюєте localTransform з тих, хто виконує якусь роботу, і пізніше намагаєтеся перетворити (localTransform * globalTransform) назад в 3 вектор3. Я можу бути абсолютно невірним, що я просто створював таке враження.
NtscCobalt

Так, я не знаю математики настільки добре, чому тест робиться з ASIN, але пов'язаний питання використовує ту саму математику, тому я вважаю, що це правильно. Я деякий час використовував цю функцію, щоб без жодних проблем.
NtscCobalt

Чи є якась конкретна причина використання atan2f у перших двох, якщо випадки та atan2 в третіх, або це помилка друку?
Маттіас Ф

10

Майк День чудово написав цей процес: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Тепер він також реалізований у glm, починаючи з версії 0.9.7.0, 02.08.2015. Перевірте реалізацію .

Щоб зрозуміти математику, слід переглянути значення, які знаходяться у вашій матриці обертання. Крім того, ви повинні знати, в якому порядку застосовувалися обертання для створення вашої матриці, щоб правильно витягти значення.

Матриця обертання від кутів Ейлера утворюється шляхом комбінування обертів навколо осей x-, y- і z. Наприклад, обертання θ градусів навколо Z може бути виконано за допомогою матриці

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

Подібні матриці існують для обертання навколо осей X і Y:

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

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

Ми можемо помножити ці матриці разом, щоб створити одну матрицю, яка є результатом усіх трьох обертів. Важливо зазначити, що порядок того, що ці матриці множать разом, є важливим, оскільки множення матриць не є комутативним . Це означає, що Rx*Ry*Rz ≠ Rz*Ry*Rx. Розглянемо один можливий порядок обертання, zyx. Коли три матриці поєднуються, це призводить до отримання матриці, яка виглядає приблизно так:

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

де Cxкосинус xкута повороту, Sx- синус xкута повороту тощо.

Тепер завдання полягає в тому, щоб витягти оригінал x, yі zзначення , які увійшли в матрицю.

Давайте спочатку виведемо xкут. Якщо ми знаємо sin(x)і cos(x), ми можемо використовувати зворотну дотичну функцію, atan2щоб повернути нам кут. На жаль, ці значення не відображаються самі по собі в нашій матриці. Але, якщо ми детальніше подивимось на елементи M[1][2]і M[2][2], можемо побачити, що ми знаємо, -sin(x)*cos(y)як добре cos(x)*cos(y). Оскільки дотична функція - це відношення протилежної та сусідньої сторін трикутника, масштабування обох значень на однакову суму (у цьому випадку cos(y)) дасть однаковий результат. Таким чином,

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

Тепер спробуємо дістати y. Ми знаємо sin(y)з M[0][2]. Якби у нас було cos (y), ми могли б використати atan2знову, але у нас в матриці цього значення немає. Однак, завдяки піфагорійській ідентичності , ми знаємо, що:

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

Отже, ми можемо розрахувати y:

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

Останнє, нам потрібно прорахувати z. Саме тут підхід Майка Дей відрізняється від попередньої відповіді. Оскільки в цей момент ми знаємо величину xта yобертання, ми можемо побудувати матрицю обертання XY та знайти кількість zобертів, необхідну для відповідності цільовій матриці. RxRyМатриця має такий чином :

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

Оскільки ми знаємо, що RxRy* Rzдорівнює нашій вхідній матриці M, ми можемо використовувати цю матрицю для повернення до Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

Оберненої матриці обертання є транспонованою , тому ми можемо розширити це:

 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 

Тепер ми можемо вирішити для sinZта cosZ, виконавши множення матриці. Нам потрібно лише обчислити елементи [1][0]і [1][1].

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

Ось повна реалізація для довідок:

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

Зауважте, однак, проблема, коли y = pi / 2 і, отже, cos (y) == 0. Тоді НЕ так, що M [1] [3] і M [2] [3] можна використовувати для отримання x тому що коефіцієнт не визначений, і не можна отримати значення atan2 . Я вважаю, що це еквівалентно проблемі з фіксацією карданчика .
Пітер Геркенс

@PieterGeerkens, ти маєш рацію, це фіксація карданчика. До речі, ваш коментар показав, що я мав помилковий друк у цьому розділі. Я маю на увазі матричні індекси з першим на 0, а оскільки вони є матрицями 3x3, останній індекс - 2, а не 3. Я виправив і M[1][3]з M[1][2], і M[2][3]з M[2][2].
Кріс

Я впевнений, що другий рядок першого стовпця прикладної комбінованої матриці - SxSyCz + CxSz, а не SxSySz + CxSz!
Озеро

@Lake, ти маєш рацію. Відредаговано.
Кріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.