Je fais pivoter un objet sur deux axes, alors pourquoi continue-t-il à tourner autour du troisième axe?

38

Je vois des questions souvent soulevées qui ont ce problème sous-jacent, mais elles sont toutes liées aux particularités d'une fonctionnalité ou d'un outil donné. Voici une tentative pour créer une réponse canonique à laquelle nous pouvons renvoyer les utilisateurs lorsque cela se présentera - avec de nombreux exemples animés! :)


Disons que nous fabriquons une caméra à la première personne. L'idée de base est qu'il faut regarder en l'air de gauche à droite et de haut en bas. Nous écrivons donc un peu de code comme ceci (en utilisant Unity comme exemple):

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

ou peut-être

// 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;

Et cela fonctionne généralement, mais avec le temps, la vue commence à être tordue. La caméra semble tourner sur son axe de roulis (z) même si nous lui avons seulement dit de tourner sur le x et le y!

Exemple animé d'une caméra à la première personne inclinée sur le côté

Cela peut aussi arriver si nous essayons de manipuler un objet devant la caméra - disons que c'est un globe terrestre que nous voulons tourner pour regarder autour de nous:

Exemple animé d'un globe incliné sur le côté

Le même problème - après un moment, le pôle Nord commence à s’éloigner à gauche ou à droite. Nous donnons notre avis sur deux axes, mais nous obtenons cette rotation confuse sur un troisième. Et cela arrive que nous appliquions toutes nos rotations autour des axes locaux de l'objet ou des axes globaux du monde.

L'inspecteur le voit également dans de nombreux moteurs: faites pivoter l'objet dans le monde et tout à coup, les chiffres changent sur un axe que nous n'avons même pas touché!

Exemple animé montrant la manipulation d'un objet à côté d'une lecture de ses angles de rotation.  L'angle z change même si l'utilisateur n'a pas manipulé cet axe.

Alors, est-ce un bug moteur? Comment pouvons-nous dire au programme que nous ne voulons pas qu'il ajoute une rotation supplémentaire?

Cela a-t-il quelque chose à voir avec les angles d'Euler? Devrais-je utiliser plutôt des quaternions ou des matrices de rotation ou des vecteurs de base?

DMGregory
la source
1
J'ai trouvé la réponse à la question suivante très utile également. gamedev.stackexchange.com/questions/123535/…
Travis Pettry

Réponses:

51

Non, ce n'est pas un bug de moteur ou un artefact d'une représentation de rotation particulière (cela peut arriver aussi, mais cet effet s'applique à tout système représentant des rotations, quaternions inclus).

Vous avez découvert un fait réel sur le fonctionnement de la rotation dans un espace tridimensionnel, et cela nous écarte de notre intuition concernant d'autres transformations telles que la traduction:

Exemple animé montrant que l'application de rotations dans un ordre différent donne des résultats différents

Lorsque nous composons des rotations sur plusieurs axes, le résultat obtenu ne correspond pas seulement à la valeur totale / nette que nous avons appliquée à chaque axe (comme on pourrait s'y attendre pour la traduction). L'ordre dans lequel nous appliquons les rotations change le résultat, chaque rotation déplaçant les axes sur lesquels les rotations suivantes sont appliquées (si elles tournent autour des axes locaux de l'objet), ou la relation entre l'objet et l'axe (si elles tournent autour du monde). axes).

Le changement des relations des axes au fil du temps peut confondre notre intuition sur ce que chaque axe est "supposé" faire. En particulier, certaines combinaisons de rotations en lacet et en tangage donnent le même résultat qu'une rotation de roulis!

Un exemple animé montrant une séquence de pitch-yaw-pitch local donne le même résultat qu'un roulis local

Vous pouvez vérifier que chaque étape tourne correctement autour de l’axe que nous avons demandé - il n’existe pas de problème moteur ni d’artefact dans notre notation qui interfère avec ou nous remet en question notre entrée - la nature sphérique (ou hypersphérique / quaternion) de la rotation signifie simplement que nos transformations "wrap" autour "les uns sur les autres. Ils peuvent être orthogonaux localement, pour de petites rotations, mais à mesure qu'ils s'empilent, nous constatons qu'ils ne sont pas globalement orthogonaux.

Ceci est particulièrement dramatique et évident pour les virages à 90 degrés comme ceux ci-dessus, mais les axes errants se glissent également dans de nombreuses petites rotations, comme le montre la question.

Alors, que faisons-nous à ce sujet?

Si vous disposez déjà d'un système de rotation en tangage, l'un des moyens les plus rapides d'éliminer les roulements non désirés consiste à modifier l'une des rotations afin qu'elle fonctionne sur les axes de transformation global ou parent au lieu des axes locaux de l'objet. De cette façon, vous ne pouvez pas avoir de contamination croisée entre les deux - un axe reste absolument contrôlé.

Voici la même séquence de pitch-yaw-pitch qui est devenue un roulement dans l'exemple ci-dessus, mais maintenant nous appliquons notre lacet autour de l'axe Y global au lieu de celui de l'objet

Exemple animé d'une tasse en rotation avec une hauteur locale et un lacet global, sans problème de roulisExemple animé de caméra à la première personne utilisant le lacet global

Ainsi, nous pouvons réparer la caméra à la première personne avec le 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 vous composez vos rotations en utilisant la multiplication, vous devez inverser l'ordre gauche / droite de l'une des multiplications pour obtenir le même effet:

// 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.

(L'ordre spécifique dépendra des conventions de multiplication de votre environnement, mais gauche = plus global / droit = ​​plus local est un choix courant)

Cela revient à stocker le lacet total net et le pas total souhaité en tant que variables float, puis à appliquer le résultat net en une seule fois, en construisant un seul quaternion ou matrice d'orientation à partir de ces angles uniquement (à condition que vous restiez totalPitchbloqué):

// 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;

ou équivalent...

// 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;

En utilisant cette division globale / locale, les rotations n'ont aucune chance de se combiner et de s'influencer, car elles sont appliquées à des ensembles d'axes indépendants.

La même idée peut aider si c'est un objet du monde que nous voulons faire pivoter. Pour un exemple comme le globe terrestre, nous voudrions souvent l’inverser et appliquer notre lacet localement (pour qu’il tourne toujours autour de ses pôles) et globalement (pour qu’il bascule vers notre vue, plutôt que vers l’Australie. , partout où il pointe ...)

Exemple animé d'un globe terrestre montrant une rotation plus sage

Limites

Cette stratégie hybride globale / locale n'est pas toujours la bonne solution. Par exemple, dans un jeu avec vol / natation 3D, vous pouvez vouloir être en mesure de pointer tout droit vers le haut et toujours tout en gardant le contrôle total. Mais avec cette configuration, vous obtiendrez un blocage du cardan : votre axe de lacet (global) deviendra parallèle à votre axe de roulis (local avant) et vous ne pourrez pas regarder à gauche ou à droite sans torsion.

Ce que vous pouvez faire à la place dans des cas comme celui-ci est d’utiliser des rotations locales pures comme dans la question précédente (pour que vos commandes aient la même apparence, peu importe l’endroit où vous regardez). nous corrigeons pour cela.

Par exemple, nous pouvons utiliser des rotations locales pour mettre à jour notre vecteur "avant", puis utiliser ce vecteur avant avec un vecteur de référence "haut" pour construire notre orientation finale. (En utilisant, par exemple, la méthode Quaternion.LookRotation de Unity ou en construisant manuellement une matrice orthonormée à partir de ces vecteurs) En contrôlant le vecteur haut, nous contrôlons le roulis ou la torsion.

Pour l'exemple de vol / natation, vous souhaiterez appliquer ces corrections progressivement. Si la vue est trop abrupte, la vue peut basculer de manière distrayante. Au lieu de cela, vous pouvez utiliser le vecteur de mise à jour actuel du lecteur et l'indiquer vers la verticale, image par image, jusqu'à ce que la vue soit de niveau. Cela peut parfois être moins nauséeux que de tourner la caméra lorsque les commandes du joueur sont inactives.

DMGregory
la source
1
Puis-je vous demander comment vous avez créé des gifs? Ils semblent beaux.
Bálint
1
@ Bálint J'utilise un programme gratuit appelé LICEcap - il vous permet d'enregistrer une partie de votre écran dans un gif. Ensuite, je découpe l'animation / ajoute un texte de légende supplémentaire / compresse le gif à l'aide de Photoshop.
DMGregory
Est-ce que cette réponse ne concerne que l'Unité? Existe-t-il une réponse plus générique sur le Web?
Posfan12
2
@ posfan12 L'exemple de code utilise la syntaxe Unity pour plus de précision, mais les situations et les mathématiques impliquées s'appliquent de la même manière dans tous les domaines. Avez-vous de la difficulté à appliquer les exemples à votre environnement?
DMGregory
2
Le calcul est déjà présent ci-dessus. À en juger par votre édition, vous écrasez juste les mauvaises parties de celle-ci. Il semble que vous utilisiez le type de vecteur défini ici . Cela inclut une méthode pour construire une matrice de rotation à partir d'un ensemble d'angles, comme décrit ci-dessus. Commencez avec une matrice d’identité et appelez TCVector::calcRotationMatrix(totalPitch, totalYaw, identity)- c’est l'équivalent de l'exemple "tout à la fois d'Euler" ci-dessus.
DMGregory
1

APRES 16 heures de fonctions parentales et de rotation lol.

Je voulais juste que le code ait un vide, en gros, en tournant en rond et aussi en se retournant avant.

Ou, en d'autres termes, l'objectif est de faire pivoter le lacet et le tangage sans aucun effet sur le roulis.

Voici les quelques qui ont réellement fonctionné dans mon application

En unité:

  1. Créer un vide. Appelez ça pour la fête.
  2. Donnez-lui un enfant vide, appelé ForPitch.
  3. Enfin, créez un cube et déplacez-le vers z = 5 (en avant). Nous pouvons maintenant voir ce que nous essayons d'éviter, ce qui entraînera une torsion du cube ("roulement" accidentel pendant que nous lacettons et tangons) .

A présent, la création de scripts dans Visual Studio, faites votre configuration et terminez avec ceci dans Update:

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

Notez que tout s'est cassé pour moi lorsque j'ai essayé de modifier l'ordre des responsabilités parentales. Ou si je change Space.World pour le objet pitch enfant (la rotation de Yaw parent ne dérange pas d'être pivotée dans Space.Self). Déroutant. Je pourrais certainement utiliser des éclaircissements sur la raison pour laquelle je devais prendre un axe local mais l’appliquer à travers l’espace mondial - pourquoi Space.Self gâte-t-il tout?

Jeh
la source
Je ne sais pas si cela est destiné à répondre à la question ou si vous demandez de l'aide pour comprendre pourquoi le code que vous avez écrit fonctionne de cette manière. Si c'est le dernier cas, vous devriez poster une nouvelle question, en vous connectant à cette page pour un contexte, si vous le souhaitez. En tant qu'indice, me.Rotate(me.right, angle, Space.World)est identique à me.Rotate(Vector3.right, angle, Space.Self, et vos transformations imbriquées sont équivalentes aux exemples "Pitch Locally, Yaw Globally" de la réponse acceptée.
DMGregory
Un peu des deux. En partie pour partager la réponse (par exemple, voici exactement comment cela a fonctionné pour moi). Et aussi pour introduire la question. Cet indice m'a fait réfléchir. Merci beaucoup!
Jeh
Incroyable, cela n'a pas été voté. Après des heures passées à lire des réponses complètes qui, bien que pédagogiques, ne fonctionnaient toujours pas, il suffisait simplement d’utiliser l’axe de rotation des objets au lieu d’un axe générique Vector3.right pour maintenir la rotation fixe sur l’objet. Incroyable, ce que je cherchais était enterré ici avec 0 votes et sur la 2e page de Google de nombreuses questions similaires.
user4779 le