Navigation▲
Tutoriel précédent : shadow mapping |
Tutoriel suivant : billboards |
I. Introduction▲
Ce tutoriel sort du contexte d'OpenGL, mais s'attaque néanmoins à un problème très courant : comment représenter les rotations ?
Dans le troisième tutoriel - les matrices, on a appris que celles-ci sont capables de tourner un point autour d'un axe donné. Bien que les matrices offrent une méthode propre pour transformer les sommets, leur gestion est difficile : comme, par exemple, l'obtention de la matrice de rotation à partir de la matrice finale.
On présentera les deux manières les plus courantes pour définir des rotations : les angles d'Euler et les quaternions. Et le plus important, on expliquera pourquoi vous devriez utiliser les quaternions.
II. Avant-propos : rotation VS orientation▲
En lisant des articles sur les rotations, vous pouvez être confus face au vocabulaire. Dans ce tutoriel :
- une orientation est un état : « l'orientation de l'objet est… » ;
- une rotation est une opération : « appliquer cette rotation à l'objet ».
Lorsque vous appliquez une rotation, vous changez l'orientation. Les deux peuvent être représentés avec les mêmes outils, menant à la confusion. Maintenant, on y va…
III. Angles d'Euler▲
Les angles d'Euler sont la méthode la plus simple de penser une orientation. Vous stockez trois orientations autour des axes X, Y et Z. C'est un concept très facile à comprendre. Vous pouvez utiliser un vec3 pour stocker la rotation :
vec3 EulerAngles( RotationAroundXInRadians, RotationAroundYInRadians, RotationAroundZInRadians);
Ces trois rotations sont ensuite appliquées successivement, habituellement dans cet ordre : en premier Y, puis Z et X (mais pas obligatoirement). L'utilisation d'un autre ordre entraîne d'autres résultats.
Une utilisation simple des angles d'Euler est l'orientation d'un personnage. Habituellement, les personnages de jeux ne tournent pas autour des axes X et Z, seulement autour de l'axe vertical. Donc, il est plus simple d'écrire, comprendre et maintenir float direction; que trois orientations différentes.
Une autre bonne utilisation des angles d'Euler correspond à une caméra FPS : vous avez un angle pour le cap (Y) et un pour le haut/bas (X). Jetez un œil au fichier common/controls.cpp pour en avoir un exemple.
Cependant, lorsque les choses deviennent plus complexes, les angles d'Euler rendront le travail difficile. Par exemple :
- l'interpolation douce entre deux orientations est compliquée. L'interpolation naïve des angles autour de X, Y et Z sera horrible ;
- l'application de plusieurs rotations est compliquée et inexacte : vous devez calculer la matrice de rotation finale et deviner les angles d'Euler de cette matrice ;
- un problème bien connu, le « blocage de cardan » (« Gimbal lock »), bloquera quelques fois vos rotations et d'autres singularités retourneront votre modèle à l'envers ;
- des angles différents donnent la même rotation (-180° et 180°, par exemple) ;
- c'est un bazar ; comme dit précédemment, le bon ordre est habituellement YZX, mais si vous utilisez une bibliothèque avec un autre ordre, vous allez avoir des soucis ;
- quelques opérations sont complexes : par exemple, la rotation de N degrés autour d'un axe spécifique.
Les quaternions sont l'outil qui résout ces problèmes pour représenter les rotations.
IV. Quaternions▲
Un quaternion est un ensemble de quatre nombres, [x, y, z, w], qui représentent les rotations de la façon suivante :
// RotationAngle est en radians
x =
RotationAxis.x *
sin(RotationAngle /
2
)
y =
RotationAxis.y *
sin(RotationAngle /
2
)
z =
RotationAxis.z *
sin(RotationAngle /
2
)
w =
cos(RotationAngle /
2
)
RotationAxis est, comme son nom l'indique, l'axe autour duquel vous souhaitez effectuer la rotation.
RotationAngle est l'angle de rotation autour de cet axe.
Donc fondamentalement, les quaternions stockent un axe de rotation et un angle de rotation, d'une façon simplifiant la combinaison des rotations.
IV-A. Lire des quaternions▲
Le format est définitivement moins intuitif que celui des angles d'Euler, mais il reste lisible : les composantes xyz correspondent grossièrement à l'axe de rotation et w est l'arc cosinus de l'angle de rotation (divisé par 2). Par exemple, imaginez que vous voyez les valeurs suivantes dans le débogueur : [0.7, 0, 0, 0.7]. x = 0.7, c'est plus grand que y et z, donc vous savez grossièrement que la rotation est principalement autour de l'axe X, et 2 * acos(0.7) = 1.59 radians, donc que la rotation est de 90°.
Pareillement, [0, 0, 0, 1] (w = 1) signifie que l'angle = 2 * acos(1) = 0, donc que c'est un quaternion unitaire, ne faisant aucune rotation.
IV-B. Opérations de bases▲
Connaître les mathématiques derrière les quaternions n'est que rarement utile : la représentation est tellement non intuitive que vous reposez habituellement sur les fonctions utilitaires qui feront les calculs pour vous. Si vous êtes intéressés, regardez les livres de mathématiques dans la page des outils et liens utiles.
IV-B-1. Comment créer un quaternion en C++ ?▲
// N'oubliez pas #include <glm/gtc/quaternion.hpp> et <glm/gtx/quaternion.hpp>
// Créer un quaternion unitaire (aucune rotation)
quat MyQuaternion;
// Spécification directe des quatre composantes
// Vous ne faites presque jamais cela directement
MyQuaternion =
quat(w,x,y,z);
// Conversion à partir des angles d'Euler (en radians) vers un quaternion
vec3 EulerAngles(90
, 45
, 0
);
MyQuaternion =
quat(EulerAngles);
// Conversion à partir d'un axe/angle
// Dans GLM l'angle doit être en degrés. Ici, donc, convertissez-le.
MyQuaternion =
gtx::quaternion::
angleAxis(degrees(RotationAngle), RotationAxis);
IV-B-2. Comment créer un quaternion en GLSL ?▲
Vous ne le faites pas. Convertissez le quaternion vers une matrice de rotation et utilisez-la dans la matrice de modèle. Vos sommets seront tournés comme d'habitude, avec la matrice MVP.
Dans quelques cas, vous pouvez réellement vouloir utiliser les quaternions en GLSL, par exemple si vous faites l'animation d'un squelette sur le GPU. Il n'y a pas de type pour les quaternions en GLSL, mais vous pouvez utiliser un vec4 et faire les mathématiques vous-même dans le shader.
IV-B-3. Comment convertir un quaternion vers une matrice ?▲
mat4 RotationMatrix =
quaternion::
toMat4(quaternion);
Vous pouvez maintenant construire votre matrice de modèle comme d'habitude :
mat4 RotationMatrix =
quaternion::
toMat4(quaternion);
...
mat4 ModelMatrix =
TranslationMatrix *
RotationMatrix *
ScaleMatrix;
// Vous pouvez maintenant utiliser ModelMatrix pour construire la matrice MVP
V. Donc, quelle méthode choisir ?▲
Choisir entre les angles d'Euler et les quaternions est difficile. Les angles d'Euler sont intuitifs pour les artistes, donc si vous écrivez un éditeur 3D, utilisez-les. Mais les quaternions sont pratiques pour les programmeurs et aussi plus rapides, donc vous devriez les utiliser dans le cœur du moteur 3D.
Le consensus général est exactement cela : utilisez les quaternions en interne et exposez les angles d'Euler à chaque fois que vous avez une interface utilisateur.
Vous allez être capable de gérer tout ce que vous voulez (ou du moins, cela sera plus facile) et vous pouvez toujours utiliser les angles d'Euler pour les entités le nécessitant (comme dit précédemment : la caméra, les humanoïdes et c'est à peu près tout) avec une simple conversion.
VI. Autres ressources▲
- Les livres dans la page des outils et liens utiles ;
- Aussi vieux qu'il puisse être, le Game Programming Gem 1 contient quelques articles géniaux sur les quaternions. Vous pouvez sûrement le trouver en ligne ;
- Une présentation de la GDC sur les rotations ;
- Le tutoriel sur les quaternions du wiki des programmeurs de jeux vidéo ;
- La FAQ sur les quaternions d'Ogre, bien que la seconde partie soit principalement spécifique à Ogre ;
- Les fichiers d'Ogre Vertor3D.h et Quaternions.cpp.
VII. Feuille de triche▲
VII-A. Comment savoir que deux quaternions sont similaires ?▲
Lors de l'utilisation d'un vecteur, le produit scalaire donne le cosinus de l'angle entre ces deux vecteurs. Si cette valeur est 1, alors les vecteurs sont dans la même direction.
Avec les quaternions, c'est la même chose :
float
matching =
quaternion::
dot(q1, q2);
if
( abs(matching-
1.0
) <
0.001
){
// q1 et q2 sont similaires
}
Vous pouvez aussi obtenir l'angle entre q1 et q2 en prenant le acos() de ce produit scalaire.
VII-B. Comment appliquer une rotation sur un point ?▲
Vous pouvez faire comme suit :
rotated_point =
orientation_quaternion *
point;
…mais si vous souhaitez calculer votre matrice de modèle, vous devriez probablement convertir le quaternion en une matrice.
Le centre de la rotation est toujours l'origine.
Si vous souhaitez tourner autour d'un autre point :
rotated_point =
origin +
(orientation_quaternion *
(point-
origin));
VII-C. Comment interpoler entre deux quaternions ?▲
Cela s'appelle SLERP : Sphérical Liner intERPolation. Avec GLM, vous pouvez l'appliquer avec la fonction mix :
glm::
quat interpolatedquat =
quaternion::
mix(quat1, quat2, 0.5
f); // ou n'importe quel autre facteur
VII-D. Comment accumuler deux rotations ?▲
C'est simple ! Multipliez les deux quaternions ensemble. L'ordre est identique à celui des matrices, c'est-à-dire l'inverse :
quat combined_rotation =
second_rotation *
first_rotation;
VII-E. Comment trouver la rotation entre deux vecteurs ?▲
(En d'autres mots : le quaternion doit tourner v1 pour qu'il corresponde à v2.)
L'idée de base est évidente :
- l'angle entre deux vecteurs est facilement trouvable : le produit scalaire donne son cosinus ;
- l'axe nécessaire est aussi facilement trouvable : c'est le produit vectoriel des deux vecteurs.
L'algorithme suivant fait exactement cela, mais gère aussi quelques cas spéciaux :
quat RotationBetweenVectors(vec3 start, vec3 dest){
start =
normalize(start);
dest =
normalize(dest);
float
cosTheta =
dot(start, dest);
vec3 rotationAxis;
if
(cosTheta <
-
1
+
0.001
f){
// cas spécifique lorsque les vecteurs ont des directions opposées :
// il n'y pas d'axe de rotation "idéal"
// Donc, devinez-en un, n'importe lequel fonctionnera tant qu'il est perpendiculaire avec start
rotationAxis =
cross(vec3(0.0
f, 0.0
f, 1.0
f), start);
if
(gtx::norm::
length2(rotationAxis) <
0.01
) // pas de chance, ils sont parallèles, essayez encore !
rotationAxis =
cross(vec3(1.0
f, 0.0
f, 0.0
f), start);
rotationAxis =
normalize(rotationAxis);
return
gtx::quaternion::
angleAxis(180.0
f, rotationAxis);
}
rotationAxis =
cross(start, dest);
float
s =
sqrt( (1
+
cosTheta)*
2
);
float
invs =
1
/
s;
return
quat(
s *
0.5
f,
rotationAxis.x *
invs,
rotationAxis.y *
invs,
rotationAxis.z *
invs
);
}
(Vous pouvez trouver cette fonction dans common/quaternion_utils.cpp.)
VII-F. J'ai besoin d'un équivalent à gluLookAt. Comment orienter un objet vers un point ?▲
Utilisez RotationBetweenVectors !
// Trouve la rotation entre l'avant de l'objet (que nous supposons vers +Z,
// mais cela dépend de votre modèle) et la direction voulue
quat rot1 =
RotationBetweenVectors(vec3(0.0
f, 0.0
f, 1.0
f), direction);
Maintenant, vous pouvez aussi souhaiter que votre objet soit droit :
// Recalcule desiredUp afin qu'il soit perpendiculaire à la direction
// Vous pouvez passer cette partie si vous souhaitez forcer desiredUp
vec3 right =
cross(direction, desiredUp);
desiredUp =
cross(right, direction);
// À cause de la première rotation, le haut est probablement complètement retourné.
// Trouve la rotation entre "up" de l'objet tourné et le haut voulu
vec3 newUp =
rot1 *
vec3(0.0
f, 1.0
f, 0.0
f);
quat rot2 =
RotationBetweenVectors(newUp, desiredUp);
Maintenant, combinez-les :
quat targetOrientation =
rot2 *
rot1; // rappelez-vous, dans l'ordre inverse.
Attention, « direction » est, bien sûr, une direction et non la position cible ! Vous pouvez calculer la direction simplement avec : targetPos - currentPos.
Une fois que vous avez l'orientation cible, vous allez probablement souhaiter effectuer une interpolation entre startOrientation et targetOrientation (vous pouvez trouver cette fonction dans common/quaternion_utils.cpp.).
VII-G. Comment utiliser LookAt mais en limitant la rotation à une certaine vitesse ? ▲
L'idée de base est d'effectuer un SLERP (utilisez glm::mix), mais de jouer avec la valeur d'interpolation afin que l'angle ne soit pas supérieur à la valeur désirée :
float
mixFactor =
maxAllowedAngle /
angleBetweenQuaternions;
quat result =
glm::gtc::quaternion::
mix(q1, q2, mixFactor);
Voici une fonction plus complexe, qui gère les cas spéciaux. Elle n'utilise même pas directement mix() comme optimisation :
quat RotateTowards(quat q1, quat q2, float
maxAngle){
if
( maxAngle <
0.001
f ){
// Aucune rotation permise. Évite une division par 0 plus tard.
return
q1;
}
float
cosTheta =
dot(q1, q2);
// q1 et q2 sont déjà égaux.
// Force q2 just to be sure
if
(cosTheta >
0.9999
f){
return
q2;
}
// Évite de prendre le long chemin autour de la sphère
if
(cosTheta <
0
){
q1 =
q1*-
1.0
f;
cosTheta *=
-
1.0
f;
}
float
angle =
acos(cosTheta);
// S'il y a seulement deux degrés de différence et que l'on en permet cinq
// alors on a fini.
if
(angle <
maxAngle){
return
q2;
}
float
fT =
maxAngle /
angle;
angle =
maxAngle;
quat res =
(sin((1.0
f -
fT) *
angle) *
q1 +
sin(fT *
angle) *
q2) /
sin(angle);
res =
normalize(res);
return
res;
}
Que vous pouvez utiliser comme suit :
CurrentOrientation =
RotateTowards(CurrentOrientation, TargetOrientation, 3.14
f *
deltaTime );
(Vous pouvez trouver cette fonction dans common/quaternion_utils.cpp.)
VIII. Remerciements▲
Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.
Navigation▲
Tutoriel précédent : shadow mapping |
Tutoriel suivant : billboards |