Tutoriel 15 - Contrôle de la caméra

Partie 2

Ogldev

Traducteur

Sylvain DOREMUS

Ce tutoriel poursuit l'implémentation du contrôle de la caméra par l'utilisateur. Il se focalise sur le contrôle à la souris. Il introduit aussi la notion de quaternion.

Commentez

- Titre : Tutoriel 15 - Contrôle de la caméra

- Auteur : Ogldev

- Traducteur : Sylvain DOREMUS

- Parution : 6 novembre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Etay Meiri. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Contexte

- II. Explication du code

- III. Sources

- IV. Remerciements

- Navigation

Navigation

Tutoriel précédent : contrôle de la caméra

Sommaire

I - Contexte

Dans ce tutoriel, nous allons terminer l'implémentation de la caméra en activant le contrôle de la direction par la souris. Il y a divers degrés de liberté associés à la conception de caméra. Nous allons permettre le degré de contrôle que l'on pourrait attendre dans un jeu à la première personne. Cela signifie que vous pourrez tourner la caméra à 360° (autour de l'axe Y), ce qui correspondrait à tourner la tête ou faire un tour complet avec le corps. Nous serons de plus capables d'incliner la caméra en haut et en bas pour pouvoir regarder au-dessus ou en dessous. Nous ne serons pas capables d'incliner la caméra jusqu'à faire un tour complet, ou de l'incliner dans l'autre sens (en vrille). Ces degrés de liberté sont du domaine des simulateurs de vol, qui sont hors de la portée de ce tutoriel. Néanmoins, nous aurons une caméra nous permettant d'explorer confortablement le monde en 3D que nous développerons dans les tutoriels suivants.

Le canon sol-air de la Seconde Guerre mondiale suivant montre le type de caméra que nous allons construire :

Ce canon possède deux axes de contrôle :

1. Il peut tourner à 360 degrés autour de l'axe (0, 1, 0). Cet angle est appelé « angle horizontal » et le vecteur est « l'axe vertical » ;

2. Il peut s'incliner vers le haut et vers le bas autour d'un vecteur parallèle au sol. Ce mouvement est limité et le canon ne peut pas effectuer de cercle complet. Cet angle est appelé « angle vertical » et le vecteur est « l'axe horizontal ». Notez qu'alors que l'axe vertical est constant (0, 1, 0), l'axe horizontal quant à lui tourne avec le canon et est toujours perpendiculaire à la cible du canon. C'est un point-clef à intégrer pour comprendre correctement les formules à venir.

L'objectif est de suivre le mouvement de la souris, de changer l'angle horizontal quand la souris se déplace à gauche et à droite, et l'angle vertical quand la souris se déplace en haut et en bas. Étant donné ces deux angles, nous voulons calculer le vecteur cible et le vecteur haut.

Tourner le vecteur cible selon l'angle horizontal est évident. En utilisant de la trigonométrie basique, nous pouvons voir que la composante Z du vecteur cible est le sinus de l'angle horizontal tandis que la composante X en est le cosinus (à ce stade la caméra regarde droit devant donc Y vaut zéro. Retournez au septième tutoriel pour avoir une image de cela.

Tourner le vecteur cible selon l'angle vertical est plus complexe, car l'axe horizontal tourne avec la caméra. L'axe horizontal peut être calculé en faisant un produit vectoriel entre l'axe vertical et le vecteur cible après qu'il a été tourné selon l'angle horizontal, mais tourner autour d'un vecteur quelconque (pour lever ou baisser le canon) peut être ardu.

Heureusement nous avons un outil mathématique extrêmement utile pour ce problème : le quaternion. Les quaternions ont été découverts en 1843 par Sir William Rovan Hamilton, un mathématicien irlandais, et sont basés sur l'ensemble des nombres complexes. Le quaternion « Q » est défini comme suit :

où i, j et k sont des nombres complexes et l'équation suivante est vraie :

En pratique, nous définissons un quaternion comme un vecteur 4D (x, y, z, w). Le conjugué du quaternion « Q » est défini comme suit :

La normalisation d'un quaternion est la même opération que pour un vecteur. Je vais décrire les étapes requises afin de tourner un vecteur autour d'un autre vecteur quelconque, en utilisant un quaternion. Plus de détails sur la preuve mathématique à l'origine de ces étapes peuvent être trouvés sur internet.

La fonction générale utilisée pour calculer un quaternion « W » représentant le vecteur « V » tourné selon un angle « a » est :

Où Q est le quaternion de rotation défini comme suit :

Après avoir calculé « W », le vecteur orienté est simplement (W.x, W.y, W.z). Un point important à noter dans le calcul de « W » est que nous devons d'abord multiplier « Q » par « V », ce qui est une multiplication quaternion par vecteur qui a pour résultat un quaternion, puis nous devons faire une multiplication quaternion par quaternion (le résultat de Q*V multiplié par le conjugué de Q). Les deux types de multiplications ne sont pas les mêmes. Le fichier math3d.cpp intègre les implémentations de ces types de multiplications.

Nous devons maintenir les angles horizontal et vertical à jour, car l'utilisateur déplace la souris dans l'écran et nous devons décider de comment les initialiser. Le choix logique est de les initialiser en fonction du vecteur cible fourni au constructeur de la caméra. Commençons par l'angle horizontal. Regardons l'image suivante, qui regarde le plan XZ vu de dessus :

Le vecteur cible est (x, z) et nous voulons calculer l'angle horizontal qui est représenté par la lettre alpha (la composante Y n'étant utile que pour l'angle vertical). Comme la longueur du cercle vaut 1, il est assez simple de voir que le sinus d'alpha est exactement z. Donc calculer l'arc sinus de z donnera alpha. En avons-nous terminé ? Pas encore. Comme z peut être dans l'intervalle [-1, 1], le résultat de l'arc sinus va de -90° à +90°. Cependant l'angle horizontal peut valoir jusque 360°. De plus, notre quaternion tourne dans le sens des aiguilles d'une montre. Cela signifie que lorsque nous tournons de 90° avec le quaternion, nous finissons à -1 sur l'axe Z, ce qui est l'opposé du sinus de 90° (qui vaut 1). À mes yeux, la manière la plus simple d'obtenir le bon résultat est d'utiliser l'arc sinus avec la valeur absolue de Z et de combiner le résultat avec le quartier du cercle dans lequel le vecteur se trouve. Par exemple, quand notre vecteur cible est (0, 1), nous calculons l'arc sinus de 1 qui est donc 90 et le retranchons de 360. Le résultat est 270. L'intervalle d'arc sinus pour des valeurs entre 0 et 1 est de 0 à 90°. En combinant cela avec le quartier spécifique du cercle, nous obtenons l'angle horizontal final.

Calculer l'angle vertical est un peu plus simple. Nous allons limiter l'intervalle de rotation de -90° (égal à 270° : regarder verticalement en haut) à +90° (regarder verticalement vers le bas). Cela signifie que nous avons juste besoin de la valeur négative de l'arc sinus de la composante Y dans le vecteur cible. Quand Y vaut 1 (regarder en haut) l'arc sinus vaut 90, nous avons juste à inverser le signe. Quand Y vaut -1 (regarder en bas), l'arc sinus vaut alors -90 et inverser son signe fait donc 90. Si vous êtes troublé, regardez une nouvelle fois l'image en remplaçant simplement Z par Y et X par Z.

II - Explication du code

Camera.cpp

1. Camera::Camera(int WindowWidth, int WindowHeight, const Vector3f& Pos, const Vector3f& Target, const Vector3f& Up)

2. {

3. m_windowWidth = WindowWidth;

4. m_windowHeight = WindowHeight;

5. m_pos = Pos;

7. m_target = Target;

8. m_target.Normalize();

10. m_up = Up;

11. m_up.Normalize();

13. Init();

14. }

Le constructeur de la caméra récupère maintenant les dimensions de la fenêtre. Nous en avons besoin afin de déplacer la souris au centre de l'écran. De plus, notez l'appel à la méthode Init() qui définit les propriétés internes à la caméra.

camera.cpp

1. void Camera::Init()

2. {

3. Vector3f HTarget(m_target.x, 0.0, m_target.z);

4. HTarget.Normalize();

6. if (HTarget.z >= 0.0f)

7. {

8. if (HTarget.x >= 0.0f)

9. {

10. m_AngleH = 360.0f - ToDegree(asin(HTarget.z));

11. }

12. else

13. {

14. m_AngleH = 180.0f + ToDegree(asin(HTarget.z));

15. }

16. }

17. else

18. {

19. if (HTarget.x >= 0.0f)

20. {

21. m_AngleH = ToDegree(asin(-HTarget.z));

22. }

23. else

24. {

25. m_AngleH = 90.0f + ToDegree(asin(-HTarget.z));

26. }

27. }

29. m_AngleV = -ToDegree(asin(m_target.y));

31. m_OnUpperEdge = false;

32. m_OnLowerEdge = false;

33. m_OnLeftEdge = false;

34. m_OnRightEdge = false;

35. m_mousePos.x = m_windowWidth / 2;

36. m_mousePos.y = m_windowHeight / 2;

38. glutWarpPointer(m_mousePos.x, m_mousePos.y);

39. }

Dans la fonction Init(), nous commençons par calculer l'angle horizontal. Nous créons un nouveau vecteur cible appelé HTarget (cible horizontale) qui est une projection du vecteur cible original sur le plan XZ. Puis nous le normalisons (car les formules décrites plus tôt utilisent un vecteur unitaire sur le plan XZ). Enfin nous vérifions à quel quartier du cercle le vecteur cible appartient et calculons l'angle final, basé sur la valeur positive de la composante Z. Ensuite nous calculons l'angle vertical, ce qui est beaucoup plus simple.

La caméra a quatre nouveaux indicateurs pour indiquer si la souris est placée sur un des bords de l'écran. Nous allons implémenter une rotation automatique dans la direction correspondante lorsque cela arrive. Cela nous permettra de tourner à 360°. Nous initialisons ces indicateurs à FALSE, car la souris commence au milieu de l'écran. Les deux lignes suivantes calculent où se trouve le centre de l'écran (basé sur les dimensions de la fenêtre), et la fonction glutWarpPointer() déplace effectivement la souris. Commencer avec la souris au centre de l'écran nous rend la vie beaucoup plus simple.

camera.cpp

1. void Camera::OnMouse(int x, int y)

2. {

3. const int DeltaX = x - m_mousePos.x;

4. const int DeltaY = y - m_mousePos.y;

6. m_mousePos.x = x;

7. m_mousePos.y = y;

9. m_AngleH += (float)DeltaX / 20.0f;

10. m_AngleV += (float)DeltaY / 20.0f;

12. if (DeltaX == 0) {

13. if (x <= MARGIN) {

14. m_OnLeftEdge = true;

15. }

16. else if (x >= (m_windowWidth - MARGIN)) {

17. m_OnRightEdge = true;

18. }

19. }

20. else {

21. m_OnLeftEdge = false;

22. m_OnRightEdge = false;

23. }

25. if (DeltaY == 0) {

26. if (y <= MARGIN) {

27. m_OnUpperEdge = true;

28. }

29. else if (y >= (m_windowHeight - MARGIN)) {

30. m_OnLowerEdge = true;

31. }

32. }

33. else {

34. m_OnUpperEdge = false;

35. m_OnLowerEdge = false;

36. }

38. Update();

39. }

Cette fonction est utilisée pour notifier à la caméra que la souris a bougé. Les paramètres correspondent à la nouvelle position de la souris. Nous commençons par calculer la différence avec la position précédente pour les axes X et Y. Puis nous stockons ces valeurs pour le prochain appel de cette fonction. Nous mettons à jour les angles horizontal et vertical en divisant les deltas. J'utilise des facteurs qui fonctionnent bien pour moi, mais sur d'autres ordinateurs vous pouvez avoir besoin d'autres valeurs pour ces facteurs. Nous améliorerons cela dans un tutoriel à venir quand nous mettrons la fréquence d'images de l'application comme facteur.

Le groupe de tests suivant met à jour les indicateurs « m_On*Edge » en fonction de la position de la souris. Il y a une marge qui enclenche le comportement de « bord » si la souris s'approche d'un des bords de l'écran. Cette marge vaut par défaut 10 pixels. Enfin, nous appelons Update() pour recalculer les vecteurs cible et haut basés sur les nouvelles valeurs des angles horizontal et vertical.

camera.cpp

1. void Camera::OnRender()

2. {

3. bool ShouldUpdate = false;

5. if (m_OnLeftEdge) {

6. m_AngleH -= 0.1f;

7. ShouldUpdate = true;

8. }

9. else if (m_OnRightEdge) {

10. m_AngleH += 0.1f;

11. ShouldUpdate = true;

12. }

14. if (m_OnUpperEdge) {

15. if (m_AngleV > -90.0f) {

16. m_AngleV -= 0.1f;

17. ShouldUpdate = true;

18. }

19. }

20. else if (m_OnLowerEdge) {

21. if (m_AngleV < 90.0f) {

22. m_AngleV += 0.1f;

23. ShouldUpdate = true;

24. }

25. }

27. if (ShouldUpdate) {

28. Update();

29. }

30. }

Cette fonction est appelée à partir de la boucle principale. Nous en avons besoin pour les cas où la souris est placée sur les bords de l'écran et ne bouge pas. Dans ce cas, il n'y a pas d'événement de souris, mais nous voulons malgré tout que la caméra continue à bouger (jusqu'à ce que la souris quitte le bord). Nous vérifions si un des indicateurs est à TRUE et mettons à jour l'angle correspondant. S'il y a eu un changement dans un des angles, nous appelons Update() pour mettre à jour les vecteurs cible et haut. Quand la souris quitte l'écran, nous le détectons dans le gestionnaire d'événements souris et mettons l'indicateur à FALSE. Notez la façon de limiter l'angle vertical entre -90° et +90°. C'est pour empêcher de faire des tours complets en regardant en haut ou en bas.

camera.cpp

1. void Camera::Update()

2. {

3. const Vector3f Vaxis(0.0f, 1.0f, 0.0f);

5. // Tourne le vecteur de vue avec l'angle horizontal autour de l'axe vertical

6. Vector3f View(1.0f, 0.0f, 0.0f);

7. View.Rotate(m_AngleH, Vaxis);

8. View.Normalize();

10. // Tourne le vecteur de vue avec l'angle vertical autour de l'axe horizontal

11. Vector3f Haxis = Vaxis.Cross(View);

12. Haxis.Normalize();

13. View.Rotate(m_AngleV, Haxis);

14. View.Normalize();

16. m_target = View;

17. m_target.Normalize();

19. m_up = m_target.Cross(Haxis);

20. m_up.Normalize();

21. }

Cette fonction met à jour les vecteurs cible et haut en fonction des angles vertical et horizontal. Nous commençons par un vecteur de vue dans l'état « reset ». Cela signifie qu'il est parallèle au sol (son angle vertical est zéro) et il regarde à droite (angle vertical à zéro, regardez l'image au-dessus). Nous mettons l'axe vertical vers le haut, puis nous tournons notre angle de vue autour, selon l'angle horizontal. Le résultat est un vecteur qui pointe dans la direction générale de la cible voulue, mais pas nécessairement à la bonne hauteur (par exemple, il est sur le plan XZ). En faisant un produit vectoriel de ce vecteur avec l'axe vertical, nous obtenons un autre vecteur sur le plan XZ, qui est orthogonal au plan créé par le vecteur vue et l'axe vertical. C'est notre nouvel axe horizontal et il est maintenant temps de tourner le vecteur vue autour de cet axe, en haut ou en bas, en fonction de l'angle vertical. Le résultat est le vecteur cible final et nous le mettons dans la variable membre correspondante. Maintenant, nous devons corriger le vecteur haut. Par exemple, si la caméra regarde en haut, le vecteur haut doit s'incliner vers l'arrière pour compenser (il doit être à 90° du vecteur cible). C'est similaire à la manière dont la tête s'incline en arrière lorsque l'on regarde haut dans le ciel. Le nouveau vecteur est calculé simplement, en faisant un autre produit vectoriel entre le vecteur cible final et l'axe horizontal. Si l'angle vertical vaut toujours zéro, alors le vecteur cible reste sur le plan XZ et le vecteur haut reste à (0, 1, 0). Si le vecteur cible est incliné en haut ou en bas, le vecteur haut va s'incliner en arrière ou en avant, respectivement.

tutorial15.cpp

1. glutGameModeString("1920x1200@32");

2. glutEnterGameMode();

Ces fonctions GLUT permettent à notre application de s'exécuter en plein écran dans le mode haute performance, appelé « mode de jeu ». Cela permet de tourner plus simplement la caméra sur 360°, car tout ce que nous avons à faire est amener la souris sur un des bords de l'écran. Notez que la résolution et le nombre de bits par pixel sont configurés en mode de jeu via une chaîne de caractères. 32 bits par pixel fournit le maximum de couleurs pour le rendu.

tutorial15.cpp

1. pGameCamera = new Camera(WINDOW_WIDTH, WINDOW_HEIGHT);

La caméra est maintenant allouée dynamiquement à cet endroit, car elle effectue un appel GLUT (glutWarpPointer). Cet appel échouera si GLUT n'a pas encore été initialisé.

tutorial15.cpp

1. glutPassiveMotionFunc(PassiveMouseCB);

2. glutKeyboardFunc(KeyboardCB);

Ici, nous enregistrons deux nouvelles fonctions de callback. Une pour la souris et l'autre pour les touches classiques du clavier (le callback pour les touches spéciales récupère les flèches directionnelles et les touches des fonctions). « Passive motion » signifie que la souris se déplace sans qu'un seul de ses boutons soit appuyé.

tutorial.cpp

1. static void KeyboardCB(unsigned char Key, int x, int y)

2. {

3. switch (Key) {

4. case 'q':

5. exit(0);

6. }

7. }

9. static void PassiveMouseCB(int x, int y)

10. {

11. pGameCamera->OnMouse(x, y);

12. }

Maintenant que nous utilisons le mode plein écran, il est plus difficile de quitter l'application. Le callback de clavier récupère l'appui sur la touche « q » pour quitter. Le callback de la souris transfère simplement la position de la souris à la caméra.

tutorial15.cpp

1. static void RenderSceneCB()

2. {

3. pGameCamera->OnRender();

À chaque fois que nous passons dans la boucle de rendu, nous devons en informer la caméra. Cela permet à celle-ci de tourner lorsque la souris ne bouge pas et est sur un bord de l'écran.

III - Sources

Vous pouvez télécharger les sources de ce projet en suivant ce lien :

Récupérer les sources

IV - Remerciements

Merci à Etay Meiri de nous permettre de traduire son tutoriel.

Merci à LittleWhite pour ses corrections et à ClaudeLELOUP pour sa relecture.

Navigation

Tutoriel précédent : contrôle de la caméra

Sommaire