Navigation▲
Tutoriel précédent : un cube texturé |
Tutoriel suivant : chargement d'un modèle |
I. Introduction▲
Bienvenue dans le sixième tutoriel !
Vous allez apprendre à utiliser le clavier et la souris pour déplacer la caméra exactement comme dans un FPS.
II. L'interface▲
Comme le code va être réutilisé dans les autres tutoriels, on va placer le code dans un fichier à part : common/controls.cpp et déclarer les fonctions dans common/controls.hpp afin que le fichier tutorial06.cpp puisse les utiliser.
Le code du fichier tutoriel06.cpp ne change pas beaucoup par rapport au tutoriel précédent. La plus grande modification est le calcul de la matrice MVP à chaque image au lieu de la calculer une unique fois. Déplacez donc ce code dans la boucle principale :
do
{
// ...
// Calcul de la matrice MVP à partir des entrées clavier et souris
computeMatricesFromInputs();
glm::
mat4 ProjectionMatrix =
getProjectionMatrix();
glm::
mat4 ViewMatrix =
getViewMatrix();
glm::
mat4 ModelMatrix =
glm::
mat4(1.0
);
glm::
mat4 MVP =
ProjectionMatrix *
ViewMatrix *
ModelMatrix;
// ...
}
Ce code nécessite trois nouvelles fonctions :
- computeMatricesFromInputs() lit le clavier et la souris et calcule les matrices de projection et de vue. C'est ici que la magie opère ;
- getProjectionMatrix() retourne simplement la matrice de projection calculée ;
- getViewMatrix() retourne simplement la matrice de vue calculée.
C'est une façon de faire. Bien sûr, si vous n'aimez pas ces fonctions, allez-y et changez-les.
Regardez maintenant ce qu'il y a dans controls.cpp.
III. Le vrai code▲
On a besoin de quelques variables :
// position
glm::
vec3 position =
glm::
vec3( 0
, 0
, 5
);
// angle horizontal : vers -Z
float
horizontalAngle =
3.14
f;
// angle vertical : 0, regarde l'horizon
float
verticalAngle =
0.0
f;
// Champ de vision initial
float
initialFoV =
45.0
f;
float
speed =
3.0
f; // trois unités/seconde
float
mouseSpeed =
0.005
f;
Le champ de vision est le niveau de zoom. 80° = un grand angle donc de grosses déformations. 60° - 45° : standard. 20° : gros zoom.
Premièrement, on va recalculer la position, l'angle horizontal, l'angle vertical et le champ de vision selon les entrées, puis on va calculer les matrices de vue et de projection à partir de la position, de l'angle horizontal, de l'angle vertical et du champ de vision.
III-A. Orientation▲
Lire les informations de la souris est facile :
// Récupère la position de la souris
int
xpos, ypos;
glfwGetMousePos(&
xpos, &
ypos);
Mais on doit prendre garde à replacer le curseur au centre de l'écran ou il ira en dehors de la fenêtre et vous ne serez plus capable de bouger.
// Réinitialise la position de la souris pour la prochaine image
glfwSetMousePos(1024
/
2
, 768
/
2
);
Ce code fait l'hypothèse que la fenêtre est de 1024 * 768, ce qui n'est évidemment pas toujours le cas. Si vous le souhaitez, vous pouvez utiliser glfwGetWindowSize pour récupérer la taille de la fenêtre.
Maintenant, on calcule nos angles de vision :
// Calcule la nouvelle orientation
horizontalAngle +=
mouseSpeed *
deltaTime *
float
(1024
/
2
-
xpos );
verticalAngle +=
mouseSpeed *
deltaTime *
float
( 768
/
2
-
ypos );
On lit ces lignes de droite à gauche :
- 1024/2 - xpos correspond à la distance de la souris par rapport au centre de la fenêtre. Plus grande est la valeur, plus nous voulons tourner ;
- float(…) convertit la position en un nombre à virgule flottante afin que la multiplication soit exacte ;
- mouseSpeed n'est là que pour ralentir ou accélérer les rotations. Modifiez cette valeur à volonté ou laissez l'utilisateur la choisir ;
- += : si vous n'avez pas déplacé la souris, 1024/2 - xpos vaudra 0 et horizontalAngle += 0 ne modifie pas la variable. Si vous aviez un '=' à la place, vous seriez retourné de force à l'orientation d'origine et cela à chaque image, ce qui n'est pas ce que l'on veut.
Maintenant, on peut calculer un vecteur qui représente, dans l'espace monde, la direction vers laquelle on regarde.
// Direction : une conversion de coordonnées sphériques vers coordonnées cartésiennes
glm::
vec3 direction(
cos(verticalAngle) *
sin(horizontalAngle),
sin(verticalAngle),
cos(verticalAngle) *
cos(horizontalAngle)
);
C'est un calcul standard, mais si vous ne connaissez pas sinus et cosinus, voici une courte explication :
La formule ci-dessus n'est qu'une généralisation pour la 3D.
Maintenant, on souhaite calculer le vecteur « haut » efficacement. Remarquez que le « haut » n'est pas toujours en direction de +Y : si vous regardez vers le bas, par exemple, le vecteur « haut » sera en réalité vertical. Voici un exemple avec deux caméras ayant la même position, la même direction, mais de vecteurs « haut » différents.
Dans ce cas, la seule constante est que le vecteur allant sur la droite est toujours horizontal. Vous pouvez vérifier cela en plaçant votre bras à l'horizontal et en regardant vers le haut, le bas et dans toutes les directions. Voici comment définir le « bon » vecteur « vers la droite » : sa coordonnée Y est 0, car le vecteur est horizontal et ses coordonnées X et Z sont exactement comme dans la figure ci-dessus, mais avec des angles tournés de 90 °, ou Pi / 2 radians.
// vecteur « vers la droite »
glm::
vec3 right =
glm::
vec3(
sin(horizontalAngle -
3.14
f/
2.0
f),
0
,
cos(horizontalAngle -
3.14
f/
2.0
f)
);
On a le vecteur « vers la droite » et une « direction », ou un vecteur vers « l'avant ». Le vecteur « haut » est le vecteur qui est perpendiculaire à ceux-ci. Il y a un outil mathématique pratique qui rend cela très simple : le produit vectoriel.
// Vecteur « haut » : perpendiculaire à la direction et le vecteur vers la droite
glm::
vec3 up =
glm::
cross( right, direction );
Pour se souvenir de ce que fait le produit vectoriel, c'est très simple. Rappelez-vous la règle de la main droite du troisième tutoriel. Le premier vecteur est le pouce ; le second l'index et le résultat est le majeur. C'est très pratique.
III-B. Position▲
Le code est facile à comprendre. D'ailleurs, j'ai utilisé les touches fléchées à la place de aswd, car je suis sur un clavier azerty et aswd correspond à zqsd. C'est aussi différent avec les claviers qwerZ et passons les claviers coréens. Je ne sais même pas quelle disposition les coréens ont, mais je suppose que c'est différent.
// Aller vers l'avant
if
(glfwGetKey( GLFW_KEY_UP ) ==
GLFW_PRESS){
position +=
direction *
deltaTime *
speed;
}
// Aller vers l'arrière
if
(glfwGetKey( GLFW_KEY_DOWN ) ==
GLFW_PRESS){
position -=
direction *
deltaTime *
speed;
}
// Pas à droite
if
(glfwGetKey( GLFW_KEY_RIGHT ) ==
GLFW_PRESS){
position +=
right *
deltaTime *
speed;
}
// Pas à gauche
if
(glfwGetKey( GLFW_KEY_LEFT ) ==
GLFW_PRESS){
position -=
right *
deltaTime *
speed;
}
La seule chose particulière est deltaTime. Vous ne souhaitez pas vous déplacer d'une unité chaque image pour la simple raison que :
- si vous avez un ordinateur rapide et que vous exécutez le programme à 60 FPS, vous vous déplacerez de 60 * vitesse unités en une seconde ;
- si vous avez un ordinateur lent et que vous exécutez le programme à 20 FPS, vous vous déplacerez de 20 * vitesse unités en une seconde.
Comme le fait d'avoir un ordinateur plus rapide n'est pas une raison pour se déplacer plus vite, vous devez faire dépendre la distance du « temps depuis la dernière image », ou « deltaTime ».
- si vous avez un ordinateur rapide et que vous exécutez le programme à 60 FPS, vous vous déplacerez de 1 / 60 * vitesse unités en une image, donc 1 * vitesse en une seconde.
- Si vous avez un ordinateur lent et que vous exécutez le programme à 20 FPS, vous vous déplacerez de 1 / 20 * vitesse unités en une image, donc 1 * vitesse en une seconde.
Ce qui est bien mieux. Le deltaTime est très facile à calculer :
double
currentTime =
glfwGetTime();
float
deltaTime =
float
(currentTime -
lastTime);
III-C. Champ de vision▲
Pour s'amuser, on peut aussi lier la roulette de la souris au champ de vision pour faire rapidement un zoom :
float
FoV =
initialFoV -
5
*
glfwGetMouseWheel();
III-D. Calculer les matrices▲
Le calcul des matrices est maintenant très simple. On utilise les mêmes fonctions que précédemment mais avec les nouveaux paramètres.
// Matrice de projection : champ de vision 45°, ration 4:3, distance d'affiche : 0.1 unit <-> 100 units
ProjectionMatrix =
glm::
perspective(FoV, 4.0
f /
3.0
f, 0.1
f, 100.0
f);
// Matrice de vue
ViewMatrix =
glm::
lookAt(
position, // La caméra est là
position+
direction, // et regarde ici : à la même position, plus la « direction »
up // la tête est vers le haut (définir à 0,-1,0 pour voir à l'envers)
);
IV. Résultat▲
V. Backface culling▲
Maintenant que vous pouvez librement vous déplacer, si vous allez dans le cube, vous allez remarquer que les polygones sont toujours affichés. Cela peut sembler évident, mais cette remarque ouvre en réalité une possibilité d'optimisation. En effet, normalement dans les applications, vous n'êtes jamais dans un cube.
L'idée consiste à dire au GPU de vérifier si la caméra est derrière ou devant le triangle. Si elle est devant, afficher le triangle ; si elle est derrière, et que le modèle est fermé et que l'on n'est pas dans le modèle, alors il y aura un autre triangle devant et personne ne le remarquera, sauf que tout sera plus rapide : deux fois moins de triangles en moyenne !
Le mieux, c'est qu'il est très facile à calculer. Le GPU calcule la normale du triangle (en utilisant le produit scalaire, vous vous rappelez ?) et vérifie si la normale est orientée vers la caméra ou non.
Cela possède un coût malheureusement : l'orientation du triangle est implicite. Cela signifie que si vous inversez deux sommets dans votre tampon, vous allez probablement avoir un trou. Mais cela vaut généralement le petit travail supplémentaire. Souvent, vous n'avez qu'à cliquer sur « inverser les normales » dans votre logiciel de modélisation 3D (qui va, en réalité, inverser les sommets et donc les normales) et tout sera parfait.
Voici comment on active le backface culling :
// Sélectionne les triangles dont les normales ne sont pas vers la caméra
glEnable(GL_CULL_FACE);
VI. Exercices▲
- Restreignez verticalAngle afin de ne pas pouvoir retourner la caméra ;
- Créez une caméra qui tourne autour de l'objet (position = ObjectCenter + ( radius * cos(time), height, radius * sin(time) ) ) ; liez le rayon/hauteur/temps au clavier/souris, ou autre ;
- Amusez-vous !
VII. Remerciements▲
Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.
Navigation▲
Tutoriel précédent : un cube texturé |
Tutoriel suivant : chargement d'un modèle |