Como extrair ângulos de Euler da matriz de transformação?

12

Eu tenho uma realização simples do mecanismo de jogo entidade / componente.
O componente Transform possui métodos para definir a posição local, rotação local, posição global e rotação global.

Se a transformação está sendo definida como nova posição global, a posição local também muda, para atualizar a posição local. Nesse caso, estou apenas aplicando a matriz local de transformação atual à matriz mundial de transformação dos pais.

Até então, não tenho problemas, posso obter a matriz de transformação local atualizada.
Mas estou lutando para atualizar a posição local e o valor de rotação na transformação. A única solução que tenho em mente é extrair os valores de conversão e rotação do localMatrix de transform.

Para tradução, é bem fácil - eu apenas uso os valores da quarta coluna. mas e a rotação?
Como extrair ângulos de Euler da matriz de transformação?

Essa solução está correta ?:
Para encontrar a rotação em torno do eixo Z, podemos encontrar diferença entre o vetor do eixo X do localTransform e o vetor do eixo X do pai.localTransform e armazenar o resultado no Delta, então: localRotation.z = atan2 (Delta.y, Delta .x);

O mesmo para rotação em torno de X e Y, basta trocar de eixo.


fonte

Respostas:

10

Normalmente eu armazeno todos os objetos como matrizes 4x4 (você poderia fazer 3x3, mas é mais fácil para mim ter apenas uma classe) em vez de traduzir para frente e para trás entre um conjunto 4x4 e 3 de vetores3s (tradução, rotação, escala). Os ângulos de Euler são notoriamente difíceis de lidar em certos cenários, então eu recomendaria o uso do Quaternions se você realmente deseja armazenar os componentes em vez de uma matriz.

Mas aqui está um código que encontrei há algum tempo que funciona. Espero que isso ajude, infelizmente não tenho a fonte original de onde encontrei isso. Não tenho idéia de quais cenários estranhos podem não funcionar. Atualmente, estou usando isso para obter a rotação do YawPitchRoll girada, matrizes 4x4 canhotas.

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

Aqui está outro tópico que encontrei ao tentar responder à sua pergunta que parecia um resultado semelhante ao meu.

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

NtscCobalt
fonte
Parece que minha solução proposta está quase certa, só não sei por que o isan não está disponível no atan2 asin.
Além disso, como isso me ajudaria, se eu armazenasse cada componente em mat4x4 separado? Como eu poderia obter e, por exemplo, ângulo de rotação de saída em torno de algum eixo?
Sua pergunta original me levou a acreditar que você está armazenando seus objetos como 3 vetores3: Tradução, Rotação e Escala. Em seguida, ao criar um localTransform com aqueles que executam algum trabalho e depois tentar converter (localTransform * globalTransform) novamente em 3 vetores3s. Eu poderia estar totalmente errado, só estava tendo essa impressão.
NtscCobalt 13/03
Sim, eu não sei a matemática o suficiente para explicar por que o pitch é feito com ASIN, mas a pergunta vinculada usa a mesma matemática, então acredito que esteja correta. Estou usando essa função há algum tempo ou sem nenhum problema.
NtscCobalt 13/03
Existe alguma razão específica para usar atan2f nos dois primeiros casos, e atan2 no terceiro, ou é um erro de digitação?
Mattias F
10

Há um ótimo artigo sobre esse processo de Mike Day: https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2012/07/euler-angles1.pdf

Agora também está implementado no glm, a partir da versão 0.9.7.0, 02/08/2015. Confira a implementação .

Para entender a matemática, você deve examinar os valores que estão na sua matriz de rotação. Além disso, você precisa saber a ordem em que as rotações foram aplicadas para criar sua matriz para extrair adequadamente os valores.

Uma matriz de rotação dos ângulos de Euler é formada pela combinação de rotações em torno dos eixos x, y e z. Por exemplo, girar θ graus em torno de Z pode ser feito com a matriz

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

Existem matrizes semelhantes para girar sobre os eixos X e Y:

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

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

Podemos multiplicar essas matrizes para criar uma matriz que é o resultado de todas as três rotações. É importante observar que a ordem em que essas matrizes são multiplicadas é importante, porque a multiplicação da matriz não é comutativa . Isso significa isso Rx*Ry*Rz ≠ Rz*Ry*Rx. Vamos considerar uma ordem de rotação possível, zyx. Quando as três matrizes são combinadas, resulta em uma matriz que se parece com isso:

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

onde Cxé o cosseno do xângulo de rotação, Sxé o seno do xângulo de rotação, etc.

Agora, o desafio é extrair o original x, ye zos valores que entraram na matriz.

Vamos primeiro entender o xângulo. Se conhecemos o sin(x)e cos(x), podemos usar a função inversa da tangente atan2para nos devolver o ângulo. Infelizmente, esses valores não aparecem sozinhos em nossa matriz. Mas, se dermos uma olhada mais atenta nos elementos M[1][2]e M[2][2], podemos ver que sabemos -sin(x)*cos(y)também cos(x)*cos(y). Como a função tangente é a razão entre os lados oposto e adjacente de um triângulo, escalar os dois valores na mesma quantidade (nesse caso cos(y)) produzirá o mesmo resultado. Portanto,

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

Agora vamos tentar obter y. Nós sabemos sin(y)de M[0][2]. Se tivéssemos cos (y), poderíamos usar atan2novamente, mas não temos esse valor em nossa matriz. No entanto, devido à identidade pitagórica , sabemos que:

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

Então, podemos calcular y:

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

Por último, precisamos calcular z. É aqui que a abordagem de Mike Day difere da resposta anterior. Como neste momento sabemos a quantidade xe a yrotação, podemos construir uma matriz de rotação XY e encontrar a quantidade de zrotação necessária para corresponder à matriz de destino. A RxRymatriz fica assim:

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

Como sabemos que RxRy* Rzé igual à nossa matriz de entrada M, podemos usar essa matriz para retornar a Rz:

M = RxRy * Rz

inverse(RxRy) * M = Rz

O inverso de uma matriz de rotação é sua transposição , para que possamos expandir isso para:

 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 

Agora podemos resolver sinZe cosZrealizar a multiplicação da matriz. Nós só precisamos calcular os elementos [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)

Aqui está uma implementação completa para referência:

#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
fonte
Observe, no entanto, o problema quando y = pi / 2 e, portanto, cos (y) == 0. Então NÃO é o caso que M [1] [3] e M [2] [3] possam ser usados ​​para obter x porque a razão é indefinida e não é possível obter um valor atan2 . Eu acredito que isso é equivalente ao problema do bloqueio do cardan .
Pieter Geerkens
@PieterGeerkens, você está certo, isso é bloqueio de cardan. Aliás, seu comentário revelou que eu tive um erro de digitação nessa seção. Refiro-me aos índices de matriz com o primeiro em 0 e, como são matrizes 3x3, o último índice é 2, não 3. Corrigi M[1][3]com M[1][2]e M[2][3]com M[2][2].
Chris
Tenho certeza de que a primeira coluna da segunda linha da matriz combinada de exemplo é SxSyCz + CxSz, não SxSySz + CxSz!
Lake
@ Lake, você está correto. Editado.
18716 Chris