Estoy girando un objeto en dos ejes, entonces, ¿por qué sigue girando alrededor del tercer eje?

38

Veo preguntas que surgen con bastante frecuencia que tienen este problema subyacente, pero todas están atrapadas en los detalles de una característica o herramienta determinada. Aquí hay un intento de crear una respuesta canónica a la que podamos referir a los usuarios cuando surja, ¡con muchos ejemplos animados! :)


Digamos que estamos haciendo una cámara en primera persona. La idea básica es que debe guiarse para mirar hacia la izquierda y hacia la derecha, y inclinar para mirar hacia arriba y hacia abajo. Entonces escribimos un poco de código como este (usando Unity como ejemplo):

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    // Yaw around the y axis using the player's horizontal input.        
    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f);

    // Pitch around the x axis using the player's vertical input.
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f);
}

o tal vez

// Construct a quaternion or a matrix representing incremental camera rotation.
Quaternion rotation = Quaternion.Euler(
                        -Input.GetAxis("Vertical") * speed,
                         Input.GetAxis("Horizontal") * speed,
                         0);

// Fold this change into the camera's current rotation.
transform.rotation *= rotation;

Y funciona principalmente, pero con el tiempo la vista comienza a torcerse. ¡La cámara parece estar girando sobre su eje de giro (z) a pesar de que solo le dijimos que girara en x e y!

Ejemplo animado de una cámara en primera persona inclinada hacia un lado

Esto también puede suceder si estamos tratando de manipular un objeto frente a la cámara; digamos que es un globo que queremos girar para mirar alrededor:

Ejemplo animado de un globo que se inclina hacia un lado

El mismo problema: después de un tiempo, el polo norte comienza a alejarse hacia la izquierda o la derecha. Estamos aportando información sobre dos ejes, pero estamos obteniendo esta rotación confusa en un tercero. Y sucede si aplicamos todas nuestras rotaciones alrededor de los ejes locales del objeto o los ejes globales del mundo.

En muchos motores también verás esto en el inspector: ¡gira el objeto en el mundo y de repente los números cambian en un eje que ni siquiera tocamos!

Ejemplo animado que muestra la manipulación de un objeto junto a una lectura de sus ángulos de rotación.  El ángulo z cambia aunque el usuario no haya manipulado ese eje.

Entonces, ¿es esto un error del motor? ¿Cómo le decimos al programa que no queremos que agregue rotación adicional?

¿Tiene algo que ver con los ángulos de Euler? ¿Debería usar Quaternions o Matrices de rotación o Vectores básicos en su lugar?

DMGregory
fuente
1
La respuesta a la siguiente pregunta me pareció muy útil también. gamedev.stackexchange.com/questions/123535/…
Travis Pettry

Respuestas:

51

No, esto no es un error del motor o un artefacto de una representación de rotación en particular (también puede ocurrir, pero este efecto se aplica a todos los sistemas que representan rotaciones, incluidos los cuaterniones).

Has descubierto un hecho real sobre cómo funciona la rotación en el espacio tridimensional, y parte de nuestra intuición sobre otras transformaciones como la traducción:

Ejemplo animado que muestra que la aplicación de rotaciones en un orden diferente da resultados diferentes

Cuando componimos rotaciones en más de un eje, el resultado que obtenemos no es solo el valor total / neto que aplicamos a cada eje (como podríamos esperar para la traducción). El orden en el que aplicamos las rotaciones cambia el resultado, ya que cada rotación mueve los ejes en los que se aplican las siguientes rotaciones (si gira alrededor de los ejes locales del objeto), o la relación entre el objeto y el eje (si gira alrededor del mundo ejes).

El cambio de las relaciones de los ejes a lo largo del tiempo puede confundir nuestra intuición sobre lo que se supone que debe hacer cada eje. En particular, ciertas combinaciones de rotaciones de guiñada y cabeceo dan el mismo resultado que una rotación de balanceo.

El ejemplo animado que muestra una secuencia de pitch-yaw-pitch local da la misma salida que un solo rollo local

Puede verificar que cada paso gire correctamente sobre el eje que solicitamos: no hay fallas o artefactos en nuestra notación que interfieran o cuestionen nuestra entrada; la naturaleza de rotación esférica (o hiperesférica / cuaternión) solo significa que nuestras transformaciones se "ajustan" alrededor "el uno del otro. Pueden ser ortogonales localmente, para pequeñas rotaciones, pero a medida que se acumulan, descubrimos que no son ortogonales a nivel mundial.

Esto es más dramático y claro para giros de 90 grados como los anteriores, pero los ejes errantes también se arrastran en muchas pequeñas rotaciones, como se demuestra en la pregunta.

¿Entonces qué hacemos al respecto?

Si ya tiene un sistema de rotación pitch-yaw, una de las formas más rápidas de eliminar el giro no deseado es cambiar una de las rotaciones para operar en los ejes de transformación global o padre en lugar de los ejes locales del objeto. De esa manera no puede contaminarse entre los dos: un eje permanece absolutamente controlado.

Aquí está la misma secuencia de pitch-yaw-pitch que se convirtió en un rollo en el ejemplo anterior, pero ahora aplicamos nuestro guiñada alrededor del eje Y global en lugar del objeto

Ejemplo animado de una taza que gira con tono local y guiñada global, sin problema de balanceoEjemplo animado de cámara en primera persona con guiñada global

Entonces podemos arreglar la cámara en primera persona con el mantra "Pitch Locally, Yaw Globally":

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f, Space.World);
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f, Space.Self);
}

Si estás combinando tus rotaciones usando la multiplicación, cambiarías el orden izquierdo / derecho de una de las multiplicaciones para obtener el mismo efecto:

// Yaw happens "over" the current rotation, in global coordinates.
Quaternion yaw = Quaternion.Euler(0f, Input.GetAxis("Horizontal") * speed, 0f);
transform.rotation =  yaw * transform.rotation; // yaw on the left.

// Pitch happens "under" the current rotation, in local coordinates.
Quaternion pitch = Quaternion.Euler(-Input.GetAxis("Vertical") * speed, 0f, 0f);
transform.rotation = transform.rotation * pitch; // pitch on the right.

(El orden específico dependerá de las convenciones de multiplicación en su entorno, pero izquierda = más global / derecha = más local es una opción común)

Esto es equivalente a almacenar el guiñada total neta y el tono total que desee como variables flotantes, luego aplicar siempre el resultado neto de una vez, construyendo un solo cuaternión o matriz de orientación nueva solo desde estos ángulos (siempre que lo mantenga totalPitchsujeto):

// Construct a new orientation quaternion or matrix from Euler/Tait-Bryan angles.
var newRotation = Quaternion.Euler(totalPitch, totalYaw, 0f);
// Apply it to our object.
transform.rotation = newRotation;

o equivalente...

// Form a view vector using total pitch & yaw as spherical coordinates.
Vector3 forward = new Vector3(
                    Mathf.cos(totalPitch) * Mathf.sin(totalYaw),
                    Mathf.sin(totalPitch),
                    Mathf.cos(totalPitch) * Mathf.cos(totalYaw));

// Construct an orientation or view matrix pointing in that direction.
var newRotation = Quaternion.LookRotation(forward, new Vector3(0, 1, 0));
// Apply it to our object.
transform.rotation = newRotation;

Usando esta división global / local, las rotaciones no tienen la oportunidad de combinarse e influenciarse entre sí, porque se aplican a conjuntos de ejes independientes.

La misma idea puede ayudar si es un objeto en el mundo que queremos rotar. Para un ejemplo como el mundo, a menudo queremos invertirlo y aplicar nuestro guiñada localmente (por lo que siempre gira alrededor de sus polos) y lanzar globalmente (por lo que se inclina hacia / lejos de nuestra vista, en lugar de hacia / lejos de Australia , donde sea que apunte ...)

Ejemplo animado de un globo que muestra una rotación con mejor comportamiento

Limitaciones

Esta estrategia híbrida global / local no siempre es la solución correcta. Por ejemplo, en un juego con vuelo / natación en 3D, es posible que desee apuntar hacia arriba / hacia abajo y aún así tener el control total. Pero con esta configuración, golpeará el bloqueo del cardán : su eje de guiñada (global hacia arriba) se vuelve paralelo a su eje de balanceo (local hacia adelante), y no tiene forma de mirar hacia la izquierda o hacia la derecha sin girar.

Lo que puede hacer en casos como este es usar rotaciones locales puras como comenzamos en la pregunta anterior (para que sus controles se sientan igual sin importar dónde esté mirando), lo que inicialmente permitirá que algo de rollo se arrastre, pero luego corregimos por ello.

Por ejemplo, podemos usar rotaciones locales para actualizar nuestro vector "hacia adelante", luego usar ese vector hacia adelante junto con un vector "hacia arriba" de referencia para construir nuestra orientación final. (Usando, por ejemplo, Unity's Quaternion.LookRotation método , o construyendo manualmente una matriz ortonormal a partir de estos vectores) Al controlar el vector hacia arriba, controlamos el giro o giro.

Para el ejemplo de vuelo / natación, querrá aplicar estas correcciones gradualmente con el tiempo. Si es demasiado abrupto, la vista puede tambalearse de manera distractora. En su lugar, puede usar el vector ascendente actual del jugador e insinuarlo hacia la vertical, cuadro por cuadro, hasta que su vista se nivele. Aplicar esto durante un turno a veces puede ser menos nauseabundo que girar la cámara mientras los controles del jugador están inactivos.

DMGregory
fuente
1
¿Puedo preguntar cómo hiciste para los gifs? Ellos se ven bien.
Bálint
1
@ Bálint Uso un programa gratuito llamado LICEcap : te permite grabar una parte de tu pantalla en un gif. Luego recorto la animación / agrego texto de subtítulo adicional / comprimo el gif usando Photoshop.
DMGregory
¿Es esta respuesta solo para Unity? ¿Hay una respuesta más genérica en la Web?
posfan12
2
@ posfan12 El código de ejemplo utiliza la sintaxis de Unity para la concreción, pero las situaciones y las matemáticas involucradas se aplican igualmente en todos los ámbitos. ¿Tiene alguna dificultad para aplicar los ejemplos a su entorno?
DMGregory
2
Las matemáticas ya están presentes arriba. A juzgar por tu edición, solo estás machacando las partes incorrectas. Parece que estás usando el tipo de vector definido aquí . Esto incluye un método para construir una matriz de rotación a partir de un conjunto de ángulos como se describió anteriormente. Comience con una matriz de identidad y llame TCVector::calcRotationMatrix(totalPitch, totalYaw, identity): esto es equivalente al ejemplo de "todo a la vez Euler" anterior.
DMGregory
1

DESPUÉS de 16 horas de funciones de crianza y rotación lol.

Solo quería que el código tuviera un vacío, básicamente, girando en círculos y también volteando hacia adelante a medida que gira.

O, en otras palabras, el objetivo es rotar el guiñada y el lanzamiento sin ningún efecto en el lanzamiento.

Aquí están los pocos que realmente funcionaron en mi aplicación

En la unidad:

  1. Crea un vacío. Llámalo ForYaw.
  2. Déle un niño vacío, llamado ForPitch.
  3. Finalmente, haz un cubo y muévelo a z = 5 (hacia adelante). Ahora podemos ver lo que estamos tratando de evitar, que será torcer el cubo ("rodar" accidentalmente mientras bostezamos y cabeceamos) .

Ahora, crea secuencias de comandos en Visual Studio, configura y termina con esto en Actualización:

objectForYaw.Rotate(objectForYaw.up, 1f, Space.World);
    objectForPitch.Rotate(objectForPitch.right, 3f, Space.World);

    //this will work on the pitch object as well
    //objectForPitch.RotateAround
    //    (objectForPitch.position, objectForPitch.right, 3f);

Tenga en cuenta que todo se rompió para mí cuando intenté variar el orden de crianza. O si cambio Space.World por el objeto de tono secundario (al padre Yaw no le importa que lo giren a través de Space.Self). Confuso. Definitivamente podría usar algunas aclaraciones sobre por qué tuve que tomar un eje local pero aplicarlo a través del espacio mundial: ¿por qué Space.Self arruina todo?

Jeh
fuente
No me queda claro si esto tiene la intención de responder la pregunta, o si está pidiendo ayuda para comprender por qué el código que ha escrito funciona de esta manera. Si es lo último, debe publicar una nueva pregunta, vinculando a esta página para el contexto si lo desea. Como pista, me.Rotate(me.right, angle, Space.World)es lo mismo me.Rotate(Vector3.right, angle, Space.Selfy sus transformaciones anidadas son equivalentes a los ejemplos de "Pitch Locally, Yaw Globally" en la respuesta aceptada.
DMGregory
Un poco de ambos. En parte para compartir la respuesta (por ejemplo, aquí es exactamente cómo me funcionó). Y también para presentar la pregunta. Esa pista me ha hecho pensar. ¡Muchas gracias!
Jeh
Increíble, esto no fue votado. Después de horas leyendo respuestas completas que, aunque educativas aún no funcionaban, la simple respuesta de usar el eje de rotación de los objetos en lugar de un eje genérico Vector3.right es todo lo que se necesitaba para mantener la rotación fija en el objeto. Lo increíble que estaba buscando fue enterrado aquí con 0 votos y en la segunda página de Google de muchas, muchas preguntas similares.
user4779