Как извлечь углы Эйлера из матрицы преобразования?

12

У меня есть простая реализация игрового движка сущности / компонента.
Компонент Transform имеет методы для установки локального положения, локального вращения, глобального положения и глобального вращения.

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

До тех пор у меня нет проблем, я могу получить обновленную матрицу локального преобразования.
Но я борюсь за то, как обновить локальное положение и значение поворота в преобразовании. Единственное решение, которое я имею в виду, - это извлечь значения перемещения и поворота из localMatrix of transform.

Для перевода это довольно просто - я просто беру значения 4-го столбца. а как насчет вращения?
Как извлечь углы Эйлера из матрицы преобразования?

Правильно ли такое решение ?:
Чтобы найти вращение вокруг оси Z, мы можем найти разницу между вектором оси X localTransform и вектором оси X parent.localTransform и сохранить результат в Delta, затем: localRotation.z = atan2 (Delta.y, Delta .Икс);

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


источник

Ответы:

10

Обычно я сохраняю все объекты как Матрицы 4x4 (вы могли бы сделать 3x3, но мне было бы проще иметь только 1 класс) вместо того, чтобы переводить туда и обратно между 4x4 и 3 наборами vector3s (Перевод, Вращение, Масштаб). Известно, что в определенных сценариях с углами Эйлера трудно справиться, поэтому я бы порекомендовал использовать кватернионы, если вы действительно хотите хранить компоненты вместо матрицы.

Но вот код, который я нашел некоторое время назад, который работает. Я надеюсь, что это помогает, к сожалению, у меня нет оригинального источника, где я нашел это. Я понятия не имею, в каких странных сценариях это может не сработать. В настоящее время я использую это для поворота 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

NtscCobalt
источник
Кажется, что мое предложенное решение почти правильно, просто не знаю, почему вместо смолы используется atan2 asin.
Кроме того, как это поможет мне, если я буду хранить каждый компонент в отдельном mat4x4? Как я мог тогда получить и, например, вывести угол поворота вокруг некоторой оси?
Исходный вопрос заставил меня поверить, что вы храните ваши объекты как 3 вектора: перевод, вращение и масштабирование. Затем, когда вы создаете localTransform из тех, кто выполняет некоторую работу, а затем пытаетесь преобразовать (localTransform * globalTransform) обратно в 3 вектора vector3. Я могу быть совершенно неправ, у меня просто такое впечатление.
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, а поскольку они представляют собой матрицы 3х3, последний индекс равен 2, а не 3. Я исправил M[1][3]с помощью M[1][2]и M[2][3]с M[2][2].
Крис
Я почти уверен, что первый столбец второй строки в примере объединенной матрицы - это SxSyCz + CxSz, а не SxSySz + CxSz!
Озеро
@ Лейк, ты прав. Ред.
Chris