Usar Quaternions para las rotaciones de OpenGL

Así que estoy escribiendo un progtwig donde los objetos se mueven alrededor del estilo spacesim, con el fin de aprender cómo mover cosas sin problemas a través del espacio 3D. Después de jugar un poco con los angularjs de Euler, parece que no son realmente apropiados para el movimiento 3D de forma libre en direcciones arbitrarias, así que decidí pasar a lo que parece ser lo mejor para el trabajo: los cuaterniones. Tengo la intención de que el objeto gire alrededor de sus ejes XYZ locales en todo momento, nunca alrededor de los ejes XYZ globales.

Intenté implementar un sistema de rotación usando cuaterniones, pero algo no funciona. Al girar el objeto a lo largo de un eje único, si no se realizaron rotaciones previas, la cosa gira bien a lo largo de un eje determinado. Sin embargo, cuando se aplica una rotación tras otra, la segunda rotación no siempre se realiza a lo largo del eje local, se supone que debe estar girando, por ejemplo, después de una rotación de alrededor de 90 ° alrededor del eje Z, una rotación alrededor del eje Y todavía tiene lugar alrededor del eje Y global, en lugar del nuevo eje Y local que está alineado con el eje X global.

Huh. Así que vamos a ir paso a paso. El error debe estar aquí en alguna parte.

PASO 1 – Entrada de captura

Pensé que sería mejor usar angularjs de Euler (o un esquema Pitch-Yaw-Roll) para capturar la entrada del jugador. Por el momento, las teclas de flecha controlan Pitch y Yaw, mientras que Q y E controlan Roll. Capturo la entrada del jugador así (estoy usando SFML 1.6):

///SPEEDS float ForwardSpeed = 0.05; float TurnSpeed = 0.5; //Rotation sf::Vector3 Rotation; Rotation.x = 0; Rotation.y = 0; Rotation.z = 0; //PITCH if (m_pApp->GetInput().IsKeyDown(sf::Key::Up) == true) { Rotation.x -= TurnSpeed; } if (m_pApp->GetInput().IsKeyDown(sf::Key::Down) == true) { Rotation.x += TurnSpeed; } //YAW if (m_pApp->GetInput().IsKeyDown(sf::Key::Left) == true) { Rotation.y -= TurnSpeed; } if (m_pApp->GetInput().IsKeyDown(sf::Key::Right) == true) { Rotation.y += TurnSpeed; } //ROLL if (m_pApp->GetInput().IsKeyDown(sf::Key::Q) == true) { Rotation.z -= TurnSpeed; } if (m_pApp->GetInput().IsKeyDown(sf::Key::E) == true) { Rotation.z += TurnSpeed; } //Translation sf::Vector3 Translation; Translation.x = 0; Translation.y = 0; Translation.z = 0; //Move the entity if (Rotation.x != 0 || Rotation.y != 0 || Rotation.z != 0) { m_Entity->ApplyForce(Translation, Rotation); } 

m_Entity es lo que estoy tratando de rotar. También contiene las matrices de cuaternión y rotación que representan la rotación del objeto.

PASO 2: actualiza el cuaternión

No estoy 100% seguro de que esta es la forma en que se supone que debe hacerse, pero esto es lo que intenté hacer en Entity :: ApplyForce ():

 //Rotation m_Rotation.x += Rotation.x; m_Rotation.y += Rotation.y; m_Rotation.z += Rotation.z; //Multiply the new Quaternion by the current one. m_qRotation = Quaternion(m_Rotation.x, m_Rotation.y, m_Rotation.z);// * m_qRotation; m_qRotation.RotationMatrix(m_RotationMatrix); 

Como puede ver, no estoy seguro de si es mejor construir un nuevo cuaternión desde los angularjs de Euler actualizados, o si se supone que debo multiplicar el cuaternión que representa el cambio con el cuaternión que representa la rotación actual general, que es la impresión Tengo cuando leo esta guía . Si este último, mi código se vería así:

 //Multiply the new Quaternion by the current one. m_qRotation = Quaternion(Rotation.x, Rotation.y, Rotation.z) * m_qRotation; 

m_Rotación es la rotación actual del objeto almacenada en formato PYR; La rotación es el cambio exigido por la entrada del jugador. De cualquier manera, sin embargo, el problema podría estar en mi implementación de mi clase Quaternion. Aquí está todo:

 Quaternion::Quaternion(float Pitch, float Yaw, float Roll) { float Pi = 4 * atan(1); //Set the values, which came in degrees, to radians for C++ trig functions float rYaw = Yaw * Pi / 180; float rPitch = Pitch * Pi / 180; float rRoll = Roll * Pi / 180; //Components float C1 = cos(rYaw / 2); float C2 = cos(rPitch / 2); float C3 = cos(rRoll / 2); float S1 = sin(rYaw / 2); float S2 = sin(rPitch / 2); float S3 = sin(rRoll / 2); //Create the final values a = ((C1 * C2 * C3) - (S1 * S2 * S3)); x = (S1 * S2 * C3) + (C1 * C2 * S3); y = (S1 * C2 * C3) + (C1 * S2 * S3); z = (C1 * S2 * C3) - (S1 * C2 * S3); } //Overload the multiplier operator Quaternion Quaternion::operator* (Quaternion OtherQuat) { float A = (OtherQuat.a * a) - (OtherQuat.x * x) - (OtherQuat.y * y) - (OtherQuat.z * z); float X = (OtherQuat.a * x) + (OtherQuat.x * a) + (OtherQuat.y * z) - (OtherQuat.z * y); float Y = (OtherQuat.a * y) - (OtherQuat.x * z) - (OtherQuat.y * a) - (OtherQuat.z * x); float Z = (OtherQuat.a * z) - (OtherQuat.x * y) - (OtherQuat.y * x) - (OtherQuat.z * a); Quaternion NewQuat = Quaternion(0, 0, 0); NewQuat.a = A; NewQuat.x = X; NewQuat.y = Y; NewQuat.z = Z; return NewQuat; } //Calculates a rotation matrix and fills Matrix with it void Quaternion::RotationMatrix(GLfloat* Matrix) { //Column 1 Matrix[0] = (a*a) + (x*x) - (y*y) - (z*z); Matrix[1] = (2*x*y) + (2*a*z); Matrix[2] = (2*x*z) - (2*a*y); Matrix[3] = 0; //Column 2 Matrix[4] = (2*x*y) - (2*a*z); Matrix[5] = (a*a) - (x*x) + (y*y) - (z*z); Matrix[6] = (2*y*z) + (2*a*x); Matrix[7] = 0; //Column 3 Matrix[8] = (2*x*z) + (2*a*y); Matrix[9] = (2*y*z) - (2*a*x); Matrix[10] = (a*a) - (x*x) - (y*y) + (z*z); Matrix[11] = 0; //Column 4 Matrix[12] = 0; Matrix[13] = 0; Matrix[14] = 0; Matrix[15] = 1; } 

Probablemente haya algo allí para hacer temblar a alguien más sabio que yo, pero no puedo verlo. Para la conversión de angularjs de Euler a un cuaternión, usé el “primer método” de acuerdo con esta fuente , lo que también parece sugerir que la ecuación crea automáticamente una unidad cuaternión (“claramente normalizada”). Para multiplicar los cuaterniones, volví a utilizar esta guía de C ++ .

PASO 3: Derivar una matriz de rotación del cuaternión

Una vez hecho esto, según la respuesta de R. Martinho Fernandes a esta pregunta , bash construir una matriz de rotación a partir del cuaternión y usar eso para actualizar la rotación de mi objeto, usando el código anterior de Quaternion :: RotationMatrix () en la siguiente línea :

 m_qRotation.RotationMatrix(m_RotationMatrix); 

Debo señalar que m_RotationMatrix es GLfloat m_RotationMatrix[16] , según los parámetros requeridos de glMultMatrix , que creo que se supone que debo usar más adelante cuando visualice el objeto. Se inicializa como:

 m_RotationMatrix = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1}; 

Creo que es la matriz de rotación OpenGL “neutral” (cada 4 valores juntos representan una columna, ¿correcto? De nuevo, obtengo esto de la página glMultMatrix ).

PASO 4 – ¡Visualiza!

Finalmente, llegamos a la función ejecutar cada ciclo para el objeto que se supone que debe mostrarlo.

 glPushMatrix(); glTranslatef(m_Position.x, m_Position.y, m_Position.z); glMultMatrixf(m_RotationMatrix); //glRotatef(m_Rotation.y, 0.0, 1.0, 0.0); //glRotatef(m_Rotation.z, 0.0, 0.0, 1.0); //glRotatef(m_Rotation.x, 1.0, 0.0, 0.0); //glRotatef(m_qRotation.a, m_qRotation.x, m_qRotation.y, m_qRotation.z); //[...] various code displaying the object's VBO glPopMatrix(); 

He dejado mis bashs fallidos anteriores allí, comentado.

Conclusión – Panda triste

Esa es la conclusión del ciclo de vida de la entrada del jugador, desde la cuna hasta la tumba manejada por OpenGL.

Obviamente no he entendido algo, ya que el comportamiento que obtengo no es el comportamiento que quiero o espero. Pero no tengo mucha experiencia en matemáticas matriciales o cuaterniones, por lo que no tengo la información necesaria para ver el error en mis formas.

¿Puede alguien ayudarme aquí?

Todo lo que has hecho es implementar efectivamente angularjs de Euler con cuaterniones. Eso no está ayudando.

El problema con los angularjs de Euler es que, cuando se calculan las matrices, cada ángulo es relativo a la rotación de la matriz que le precedió. Lo que quiere es tomar la orientación actual de un objeto y aplicar una rotación a lo largo de algún eje, produciendo una nueva orientación.

No puedes hacer eso con los angularjs de Euler. Puedes hacerlo con matrices, y puedes hacerlo con cuaterniones (ya que son solo la parte de rotación de una matriz). Pero no puedes hacerlo fingiendo que son angularjs de Euler.

Esto se hace al no almacenar angularjs en absoluto . En cambio, solo tienes un cuaternión que representa la orientación actual del objeto. Cuando decides aplicarle una rotación (de algún ángulo por algún eje), construyes un cuaternión que representa esa rotación en un ángulo alrededor de ese eje. Luego, a la derecha, multiplica ese cuaternión con el cuaternión de orientación actual, produciendo una nueva orientación actual.

Cuando renderiza el objeto, usa la orientación actual como … la orientación.

Los cuaterniones representan orientaciones alrededor de ejes compuestos 3D. Pero también pueden representar ‘rotaciones delta’.

Para ‘rotar una orientación’, necesitamos una orientación (un quat) y una rotación (también un quat), y los multiplicamos juntos, lo que da como resultado (lo adivinaste) un quat.

Has notado que no son conmutativos, eso significa que el orden en que los multiplicamos es absolutamente importante, al igual que para las matrices. El orden tiende a depender de la implementación de tu biblioteca matemática, pero en realidad, solo hay dos maneras posibles de hacerlo, por lo que no debería tomarte demasiado tiempo descubrir cuál es la correcta, si las cosas están “en órbita”. en lugar de ‘rotar’, entonces los tienes al revés.

Para su ejemplo de guiñada y inclinación, construiría mi cuaternión de rotación delta a partir de los angularjs de guiñada, inclinación y balanceo, con el balanceo ajustado a cero, y luego lo aplicaría a mi cuaternión de “orientación”, en lugar de hacer las rotaciones un eje a la vez