c++ - Implementando una cámara compleja basada en rotación
math matrix (4)
Estoy implementando un motor 3D para visualización espacial, y estoy escribiendo una cámara con las siguientes funciones de navegación:
- Gire la cámara (es decir, análoga a girar la cabeza)
- Gire alrededor de un punto 3D arbitrario (un punto en el espacio, que probablemente no esté en el centro de la pantalla; la cámara necesita girar alrededor manteniendo la misma dirección de observación relativa, es decir, la dirección de la mirada también cambia. Esto no mira directamente a el punto de rotación elegido)
- Panorámica en el plano de la cámara (por lo tanto, mueva hacia arriba / abajo o hacia la izquierda / derecha en el plano ortogonal al vector de aspecto de la cámara)
La cámara no debe rodar, es decir, ''arriba'' permanece activa. Debido a esto, represento la cámara con una ubicación y dos ángulos, rotaciones alrededor de los ejes X e Y (Z sería en rollo). La matriz de vista se vuelve a calcular utilizando la ubicación de la cámara y estos dos ángulos. Esto funciona muy bien para panoramizar y girar el ojo, pero no para girar alrededor de un punto arbitrario. En cambio, obtengo el siguiente comportamiento:
- El ojo en sí parece moverse más arriba o abajo de lo que debería
- El ojo no se mueve hacia arriba o hacia abajo en absoluto cuando
m_dRotationX
es 0 o pi. (Gimbal lock? ¿Cómo puedo evitar esto?) - La rotación del ojo se invierte (al cambiar la rotación se ve más arriba cuando debe mirar hacia abajo, hacia abajo cuando debe mirar hacia arriba) cuando
m_dRotationX
está entre pi y 2pi.
(a) ¿Qué está causando esta ''deriva'' en la rotación?
Esto puede ser un bloqueo cardánico . Si es así, la respuesta estándar a esto es ''usar cuaterniones para representar la rotación'', dicho muchas veces aquí en SO ( 1 , 2 , 3 por ejemplo), pero desafortunadamente sin detalles concretos ( example . Esta es la mejor respuesta que he encontrado hasta ahora; es raro.) He luchado por implementar una cámara utilizando cuaterniones que combinen los dos tipos de rotaciones anteriores. De hecho, estoy construyendo un cuaternión usando las dos rotaciones, pero un comentarista a continuación dijo que no había ninguna razón: está bien construir la matriz de inmediato.
Esto ocurre cuando se cambian las rotaciones X e Y (que representan la dirección de observación de la cámara) cuando se gira alrededor de un punto, pero no ocurre simplemente al cambiar directamente las rotaciones, es decir, al girar la cámara alrededor de sí mismo. Para mí, esto no tiene sentido. Son los mismos valores.
(b) ¿Sería mejor un enfoque diferente (cuaterniones, por ejemplo) para esta cámara? Si es así, ¿cómo implemento las tres funciones de navegación de cámara anteriores?
Si un enfoque diferente sería mejor, considere proporcionar un ejemplo concretamente implementado de ese enfoque. (Estoy usando DirectX9 y C ++, y la biblioteca D3DX * que proporciona el SDK). En este segundo caso, agregaré y otorgaré una recompensa en un par de días cuando pueda agregar uno a la pregunta. Esto puede sonar como que me estoy volviendo loco, pero tengo poco tiempo y necesito implementarlo o resolverlo rápidamente (este es un proyecto comercial con una fecha límite ajustada). Una respuesta detallada también mejorará los archivos SO, porque la mayoría las respuestas de la cámara que he leído hasta ahora son claras en el código.
Gracias por tu ayuda :)
Algunas aclaraciones
Gracias por los comentarios y respuesta hasta ahora! Trataré de aclarar algunas cosas sobre el problema:
La matriz de vista se vuelve a calcular desde la posición de la cámara y los dos ángulos cada vez que una de esas cosas cambia. La matriz en sí misma nunca se acumula (es decir, se actualiza); se vuelve a calcular de nuevo. Sin embargo, la posición de la cámara y las dos variables angulares se acumulan (cada vez que se mueve el mouse, por ejemplo, uno o ambos ángulos tendrán una pequeña cantidad agregada o restada, según el número de píxeles que el mouse movió hacia arriba y hacia abajo / o izquierda-derecha en pantalla.)
El comentarista JCooper dice que JCooper bloqueo de cardán y necesito:
agregue otra rotación en su transformada que haga girar los eyePos para estar completamente en el plano yz antes de aplicar la transformación, y luego otra rotación que lo mueva hacia atrás luego. Gire alrededor del eje y en el siguiente ángulo inmediatamente antes y después de aplicar la matriz de balanceo de cabeceo de guiñada (uno de los ángulos necesitará ser negado; probarlo es la forma más rápida de decidir cuál).
double fixAngle = atan2(oEyeTranslated.z,oEyeTranslated.x);
Desafortunadamente, al implementar esto como se describe, mi ojo se dispara por encima de la escena a un ritmo muy rápido debido a una de las rotaciones. Estoy seguro de que mi código es simplemente una mala implementación de esta descripción, pero aún necesito algo más concreto. En general, encuentro descripciones de texto no específicas de algoritmos que son menos útiles que las implementaciones comentadas y explicadas. Estoy agregando un bounty para un ejemplo concreto y funcional que se integre con el siguiente código (es decir, con los otros métodos de navegación también). Esto es porque me gustaría entender la solución, además de tener algo que funciona, y porque Necesito implementar algo que funcione rápidamente ya que estoy en un plazo apretado.
Por favor, si responde con una descripción de texto del algoritmo, asegúrese de que sea lo suficientemente detallado como para implementarlo (''Girar alrededor de Y, luego transformar, luego girar hacia atrás'' puede tener sentido para usted, pero le faltan detalles para saber lo que quiere decir. las respuestas son claras, señalizadas, permitirán que otros entiendan incluso con una base diferente, son ''paneles de información resistentes a la intemperie'' ).
A cambio, he tratado de ser claro al describir el problema, y si puedo aclararlo, házmelo saber.
Mi código actual
Para implementar las tres funciones de navegación anteriores, en un evento de movimiento del mouse que se mueve en función de los píxeles que ha movido el cursor:
// Adjust this to change rotation speed when dragging (units are radians per pixel mouse moves)
// This is both rotating the eye, and rotating around a point
static const double dRotatePixelScale = 0.001;
// Adjust this to change pan speed (units are meters per pixel mouse moves)
static const double dPanPixelScale = 0.15;
switch (m_eCurrentNavigation) {
case ENavigation::eRotatePoint: {
// Rotating around m_oRotateAroundPos
const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
// To rotate around the point, translate so the point is at (0,0,0) (this makes the point
// the origin so the eye rotates around the origin), rotate, translate back
// However, the camera is represented as an eye plus two (X and Y) rotation angles
// This needs to keep the same relative rotation.
// Rotate the eye around the point
const D3DXVECTOR3 oEyeTranslated = m_oEyePos - m_oRotateAroundPos;
D3DXMATRIX oRotationMatrix;
D3DXMatrixRotationYawPitchRoll(&oRotationMatrix, dX, dY, 0.0);
D3DXVECTOR4 oEyeRotated;
D3DXVec3Transform(&oEyeRotated, &oEyeTranslated, &oRotationMatrix);
m_oEyePos = D3DXVECTOR3(oEyeRotated.x, oEyeRotated.y, oEyeRotated.z) + m_oRotateAroundPos;
// Increment rotation to keep the same relative look angles
RotateXAxis(dX);
RotateYAxis(dY);
break;
}
case ENavigation::ePanPlane: {
const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dPanPixelScale;
const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dPanPixelScale;
m_oEyePos += GetXAxis() * dX; // GetX/YAxis reads from the view matrix, so increments correctly
m_oEyePos += GetYAxis() * -dY; // Inverted compared to screen coords
break;
}
case ENavigation::eRotateEye: {
// Rotate in radians around local (camera not scene space) X and Y axes
const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
const double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
RotateXAxis(dX);
RotateYAxis(dY);
break;
}
Los métodos RotateXAxis
y RotateYAxis
son muy simples:
void Camera::RotateXAxis(const double dRadians) {
m_dRotationX += dRadians;
m_dRotationX = fmod(m_dRotationX, 2 * D3DX_PI); // Keep in valid circular range
}
void Camera::RotateYAxis(const double dRadians) {
m_dRotationY += dRadians;
// Limit it so you don''t rotate around when looking up and down
m_dRotationY = std::min(m_dRotationY, D3DX_PI * 0.49); // Almost fully up
m_dRotationY = std::max(m_dRotationY, D3DX_PI * -0.49); // Almost fully down
}
Y para generar la matriz de vista a partir de esto:
void Camera::UpdateView() const {
const D3DXVECTOR3 oEyePos(GetEyePos());
const D3DXVECTOR3 oUpVector(0.0f, 1.0f, 0.0f); // Keep up "up", always.
// Generate a rotation matrix via a quaternion
D3DXQUATERNION oRotationQuat;
D3DXQuaternionRotationYawPitchRoll(&oRotationQuat, m_dRotationX, m_dRotationY, 0.0);
D3DXMATRIX oRotationMatrix;
D3DXMatrixRotationQuaternion(&oRotationMatrix, &oRotationQuat);
// Generate view matrix by looking at a point 1 unit ahead of the eye (transformed by the above
// rotation)
D3DXVECTOR3 oForward(0.0, 0.0, 1.0);
D3DXVECTOR4 oForward4;
D3DXVec3Transform(&oForward4, &oForward, &oRotationMatrix);
D3DXVECTOR3 oTarget = oEyePos + D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z); // eye pos + look vector = look target position
D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector);
}
Creo que hay una solución mucho más simple que le permite eludir todos los problemas de rotación.
Notación: A es el punto que queremos rotar, C es la ubicación original de la cámara, M es la matriz de rotación de la cámara original que asigna coordenadas globales a la ventana gráfica local de la cámara.
- Tome nota de las coordenadas locales de A , que son iguales a A ''= M × ( A - C ).
- Gire la cámara como lo haría en el modo normal de "rotación de ojos". Actualice la matriz de vista M para que se modifique a M 2 y C permanezca sin cambios.
- Ahora nos gustaría encontrar C 2 tal que A ''= M 2 × ( A - C 2 ).
Esto se hace fácilmente mediante la ecuación C 2 = A - M 2 -1 × A '' . - Voilà, la cámara se ha girado y debido a que las coordenadas locales de A no se modifican, A permanece en la misma ubicación y con la misma escala y distancia.
Como una ventaja adicional, el comportamiento de rotación ahora es consistente entre el modo de "rotación de ojos" y "rotación de puntos".
Me parece que "Roll" no debería ser posible dada la forma en que se forma la matriz de visualización. Independientemente de todos los demás códigos (algunos de los cuales se ven un poco graciosos), la llamada D3DXMatrixLookAtLH(&m_oViewMatrix, &oEyePos, &oTarget, &oUpVector);
debería crear una matriz sin rollo cuando se le dé [0,1,0]
como un vector ''Arriba'' a menos que oTarget-oEyePos
ser paralelo al vector ascendente. Esto no parece ser el caso, ya que está restringiendo que m_dRotationY
esté dentro de (-.49pi, + .49pi).
Quizás puedas aclarar cómo sabes que está sucediendo el ''roll''. ¿Tienes un plano de tierra y la línea del horizonte de ese plano de tierra se está alejando de la horizontal?
Como un lado, en UpdateView
, el D3DXQuaternionRotationYawPitchRoll
parece completamente innecesario ya que de inmediato se da vuelta y lo cambia a una matriz. Simplemente use D3DXMatrixRotationYawPitchRoll
como lo hizo en el evento del mouse. Los cuaterniones se usan en las cámaras porque son una forma conveniente de acumular rotaciones en las coordenadas del ojo. Como solo está utilizando dos ejes de rotación en un orden estricto, su forma de acumular ángulos debería estar bien. La transformación vectorial de (0,0,1) tampoco es realmente necesaria. La oRotationMatrix
ya debería tener esos valores en las (_31,_32,_33)
.
Actualizar
Dado que no es rollo, aquí está el problema: creas una matriz de rotación para mover el ojo en coordenadas mundiales , pero quieres que el tono ocurra en las coordenadas de la cámara . Como el giro no está permitido y la guiñada se realiza al final, la orientación es siempre la misma tanto en el mundo como en los marcos de referencia de la cámara. Considera las imágenes a continuación:
Su código funciona bien para el cabeceo y el guiñada local, ya que estos se logran en las coordenadas de la cámara.
Pero cuando gira alrededor de un punto de referencia, está creando una matriz de rotación que está en coordenadas mundiales y la usa para rotar el centro de la cámara. Esto funciona bien si el sistema de coordenadas de la cámara coincide con el del mundo. Sin embargo, si no comprueba si está contra el límite de paso antes de rotar la posición de la cámara, obtendrá un comportamiento loco cuando llegue a ese límite. La cámara de repente comenzará a patinar alrededor del mundo, aún ''girando'' alrededor del punto de referencia, pero sin cambiar la orientación.
Si los ejes de la cámara no se alinean con los del mundo, sucederán cosas extrañas. En el caso extremo, la cámara no se moverá del todo porque está tratando de hacerla rodar.
Lo anterior es lo que normalmente sucedería, pero como maneja la orientación de la cámara por separado, la cámara no se mueve.
En cambio, se mantiene vertical, pero se produce una traducción extraña.
Una forma de manejar esto sería (1) colocar siempre la cámara en una posición canónica y orientación relativa al punto de referencia, (2) hacer su rotación, y luego (3) volver a colocarla cuando haya terminado (por ejemplo, similar a la forma en que traduces el punto de referencia al origen, aplica la rotación del giro de guiñada y luego traduce hacia atrás). Sin embargo, al pensar más sobre esto, probablemente esta no sea la mejor manera de hacerlo.
Actualización 2
Creo que la respuesta de Genérico Humano es probablemente la mejor. La pregunta sigue siendo sobre cuánto tono se debe aplicar si la rotación está fuera del eje, pero por ahora, vamos a ignorar eso. Tal vez te dé resultados aceptables.
La esencia de la respuesta es esta: antes del movimiento del mouse, su cámara está en c 1 = m_oEyePos
y está orientada por M 1 = D3DXMatrixRotationYawPitchRoll(&M_1,m_dRotationX,m_dRotationY,0)
. Considere el punto de referencia a = m_oRotateAroundPos
. Desde el punto de vista de la cámara, este punto es a ''= M 1 (ac 1 ) .
Desea cambiar la orientación de la cámara a M 2 = D3DXMatrixRotationYawPitchRoll(&M_2,m_dRotationX+dX,m_dRotationY+dY,0)
. [ Importante: dado que no permitirá que m_dRotationY
fuera de un rango específico, debe asegurarse de que dY no viole esa restricción.] A medida que la cámara cambia de orientación, también desea que su posición gire alrededor de una a una nueva punto c 2 . Esto significa que a no cambiará desde la perspectiva de la cámara. Es decir, M 1 (ac 1 ) == M 2 (ac 2 ) .
Entonces resolvemos para c 2 (recuerde que la transposición de una matriz de rotación es la misma que la inversa):
M 2 T M 1 (ac 1 ) == (ac 2 ) =>
-M 2 T M 1 (ac 1 ) + a == c 2
Ahora, si vemos esto como una transformación que se aplica a c 1 , entonces podemos ver que primero se niega, luego se traduce por a , luego se rota por M 1 , luego se rota por M 2 T , se niega de nuevo, y luego se traduce por una vez más. Estas son transformaciones en las que las bibliotecas de gráficos son buenas y todas se pueden agrupar en una sola matriz de transformación.
@Generic Human merece crédito por la respuesta, pero aquí hay un código para ello. Por supuesto, debe implementar la función para validar un cambio en el tono antes de aplicarlo, pero eso es simple. Este código probablemente tiene un par de errores tipográficos ya que no he intentado compilar:
case ENavigation::eRotatePoint: {
const double dX = (double)(m_oLastMousePos.x - roMousePos.x) * dRotatePixelScale * D3DX_PI;
double dY = (double)(m_oLastMousePos.y - roMousePos.y) * dRotatePixelScale * D3DX_PI;
dY = validatePitch(dY); // dY needs to be kept within bounds so that m_dRotationY is within bounds
D3DXMATRIX oRotationMatrix1; // The camera orientation before mouse-change
D3DXMatrixRotationYawPitchRoll(&oRotationMatrix1, m_dRotationX, m_dRotationY, 0.0);
D3DXMATRIX oRotationMatrix2; // The camera orientation after mouse-change
D3DXMatrixRotationYawPitchRoll(&oRotationMatrix2, m_dRotationX + dX, m_dRotationY + dY, 0.0);
D3DXMATRIX oRotationMatrix2Inv; // The inverse of the orientation
D3DXMatrixTranspose(&oRotationMatrix2Inv,&oRotationMatrix2); // Transpose is the same in this case
D3DXMATRIX oScaleMatrix; // Negative scaling matrix for negating the translation
D3DXMatrixScaling(&oScaleMatrix,-1,-1,-1);
D3DXMATRIX oTranslationMatrix; // Translation by the reference point
D3DXMatrixTranslation(&oTranslationMatrix,
m_oRotateAroundPos.x,m_oRotateAroundPos.y,m_oRotateAroundPos.z);
D3DXMATRIX oTransformMatrix; // The full transform for the eyePos.
// We assume the matrix multiply protects against variable aliasing
D3DXMatrixMultiply(&oTransformMatrix,&oScaleMatrix,&oTranslationMatrix);
D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix1);
D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oRotationMatrix2Inv);
D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oScaleMatrix);
D3DXMatrixMultiply(&oTransformMatrix,&oTransformMatrix,&oTranslationMatrix);
D3DXVECTOR4 oEyeFinal;
D3DXVec3Transform(&oEyeFinal, &m_oEyePos, &oTransformMatrix);
m_oEyePos = D3DXVECTOR3(oEyeFinal.x, oEyeFinal.y, oEyeFinal.z)
// Increment rotation to keep the same relative look angles
RotateXAxis(dX);
RotateYAxis(dY);
break;
}
Si entiendo correctamente, está satisfecho con el componente de rotación en la matriz final (salvo los controles de rotación invertida en el problema n. ° 3), pero no con la parte de traducción, ¿es así?
El problema parece provenir del hecho de que los trata de manera diferente: está recalculando la parte de rotación desde cero cada vez, pero acumula la parte de traducción (m_oEyePos). Otros comentarios mencionan problemas de precisión, pero en realidad es más significativo que solo la precisión de FP: acumular rotaciones a partir de pequeños valores de guiñada / inclinación simplemente no es lo mismo-matemáticamente-que hacer una gran rotación desde la inclinación / inclinación acumulada. De ahí la discrepancia de rotación / traducción. Para solucionar este problema, intente volver a calcular la posición del ojo desde cero simultáneamente con la parte de rotación, de forma similar a cómo se encuentra "oTarget = oEyePos + ...":
oEyePos = m_oRotateAroundPos - dist * D3DXVECTOR3(oForward4.x, oForward4.y, oForward4.z)
dist
puede ser fijo o calculado desde la posición del ojo viejo. Eso mantendrá el punto de rotación en el centro de la pantalla; en el caso más general (que le interese), -dist * oForward
here debería reemplazarse por el m_oEyePos - m_oRotateAroundPos
anterior / inicial multiplicado por la rotación de cámara anterior / inicial para llevarlo al espacio de la cámara (encontrando un vector de desplazamiento constante en el sistema de coordenadas de la cámara), luego se multiplica por la nueva rotación de la cámara invertida para obtener la nueva dirección en el mundo.
Esto, por supuesto, estará sujeto a bloqueo cardánico cuando el lanzamiento esté hacia arriba o hacia abajo. Deberá definir con precisión qué comportamiento espera en estos casos para resolver esta parte. Por otro lado, bloquear m_dRotationX = 0 o = pi es bastante extraño (esto es yaw, no pitch, ¿no?) Y podría estar relacionado con lo anterior.
Usted gira alrededor del punto aplicando repetidamente pequeñas matrices de rotación, esto probablemente causa la deriva (los pequeños errores de precisión se suman) y apuesto a que no hará un círculo perfecto después de un tiempo. Como los ángulos de la vista usan doble simple de 1 dimensión, tienen mucha menos deriva.
Una posible solución sería almacenar un guiñada / inclinación específica y una posición relativa desde el punto cuando ingresas a ese modo de vista, y usarlos para hacer los cálculos. Esto requiere un poco más de contabilidad, ya que debe actualizarlos al mover la cámara. Tenga en cuenta que también hará que la cámara se mueva si el punto se mueve, lo cual creo que es una mejora.