• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 13/03/2017

La caméra

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

J'ai une bien mauvaise nouvelle très chers compagnons, nous arrivons à la fin de la première partie de ce tutoriel. :(
(Super façon d'introduire un chapitre :-° )

En effet, nous avons déjà vu presque toutes les notions essentielles d'OpenGL. Nous maitrisons l'affichage de formes simples, l'utilisation basique de Shaders, l'affichage de textures, la gestion des évènements, ... Il ne reste plus qu'une seule chose à voir pour compléter ces essentielles : une caméra déplaçable.

Nous avons déjà eu l'occasion d'approcher la caméra, notamment dans le chapitre sur la 3D, mais jusqu'à maintenant on se contentait de la positionner au début du programme et du coup elle restait immobile. Notre objectif, avec ce chapitre, va être de lui donner la capacité de se déplacer. Ainsi nous pourrons nous balader librement dans nos scènes 3D.

Après ce chapitre, nous passerons à un TP qui rassemblera toutes les notions ce que nous avons vu. Nous aurons un début de monde 3D réaliste. Mais bon pour le moment, nous devons encore étudier la caméra.

Commençons ! :magicien:

Fonctionnement

Une caméra déplaçable

Les types de caméra

On a déjà eu l’occasion d'approcher le système caméra avec OpenGL qui permet d'avoir une représentation d'un monde virtuel sur notre écran. Ce système fait exactement la même chose qu'un tournage de film, nous ne sommes pas présents sur un plateau et pourtant nous voyons les scènes qui y sont tournées.

Le problème de notre caméra actuelle c'est qu'elle est totalement immobile, nous n'avons aucun moyen pour la faire bouger. L'objectif de ce chapitre va donc être de lui ajouter de la mobilité afin que nous puissions nous déplacer dans notre monde virtuel. Nous utiliserons une méthode de déplacement particulière basée sur le vol en mode libre (FreeFly en anglais). Ce mode permet à la caméra de voler, tel un avion, de façon totalement libre. On pourra donc aller à gauche, à droite, en haut et en bas comme on voudra. :p

On retrouve ce type de caméra dans les jeux-vidéo où on pilote un hélicoptère, un avion, ...

Image utilisateur

Image issue du jeu GTA San Andreas

Cette caméra représentera une base avec laquelle nous pourrons travailler. Dans l'avenir, il suffira de quelques modifications pour la faire passer en vue à la 1ière ou à la 3ième personne. Il existe aussi d'autres types caméras en vue de dessus que l'on remarque dans les jeux de stratégie par exemple. Mais pour le moment, nous nous contenterons de développer une caméra libre que nous utiliserons dans les futurs chapitres. :)

Les contrôles

Pour pouvoir se déplacer dans notre monde 3D, nous devrons utiliser le clavier et la souris. Grâce à la classe Input (que l'on a déjà codée) nous allons pouvoir lier un évènement à un déplacement, ces évènements seront divisés en deux catégories. Nous utiliserons :

  • La souris : pour orienter la caméra en fonction du mouvement effectué

  • Le clavier : pour déplacer la caméra selon 4 axes (gauche, droite, haut et bas)

Ces deux catégories d'évènements vont se gérer de manière totalement différente, nous aurons besoin de deux méthodes distinctes. Et croyez-moi, elles ne feront pas du tout la même chose. :p

La gestion de l'orientation

On vient de voir le principe d'une caméra libre et je vous ai même précisé la façon dont on va la gérer. Je vais maintenant aller plus loin dans la théorie afin que vous puissiez comprendre ce que nous allons faire par la suite. Ouvrez grand les oreilles et n'hésitez pas à relire plusieurs fois ce début de chapitre. Il est un peu technique mais il est absolument fondamental que vous compreniez les notions que l'on va aborder. D’ailleurs, on en ré-utilisera certaines dans de futurs chapitres.

La première chose que l'on va voir concerne l'orientation de la caméra. Les évènements déclenchés par la souris permettront de l'orienter en fonction du mouvement effectué. Lorsque nous bougerons la souris vers le haut ou vers le bas, la caméra s'orientera de la même façon :

Image utilisateur

Et lorsque nous bougerons la souris vers la gauche ou vers la droite, la caméra s'orientera également de la même façon :

Image utilisateur

Bien entendu, on peut combiner ces deux mouvements pour que la caméra s'oriente dans n'importe quelle direction. Voici un schéma résumant toutes les informations dont nous avons besoin sur l'orientation :

Image utilisateur

Les angles Phi et Theta représentent les éléments mathématiques indispensables qui vont nous permettre de calculer l'orientation de la caméra. Le premier représente l'orientation sur l'axe horizontal, le second l'orientation sur l'axe vertical. Ces deux angles sont absolument primordiaux. Sans eux, on pourrait toujours se déplacer vers la gauche ou la droite, ... mais notre regard resterait totalement fixe. Imaginez-vous en train de marcher dans la rue sans pouvoir tourner la tête (un peu embêtant n'est-ce pas ?).

Les angles ne sont pas les seuls éléments important sur ce schéma, le vecteur V (orientation) est tout aussi important. Il représente la direction de notre regard (ce qui sera affiché sur l'écran).

Mais avec quoi va-t-on pouvoir calculer ce vecteur orientation ? Je sais pas le calculer moi. :(

Rassurez-vous, je ne vais pas vous demander de trouver une formule par vous-même. :p

En fait, il existe une formule toute prête pour calculer ce vecteur, enfin plus précisément pour calculer ses coordonnées (x, y, z). Cette formule permet de trouver ce que l'on appelle les coordonnées sphériques :

Image utilisateur

Ah tiens ! On retrouve les angles de tout à l'heure !

Et oui ah ah ! Je vous avais dit que ces angles étaient super importants. :p Et encore, on peut simplifier cette fonction en enlevant le paramètre R. Ce paramètre correspond à la longueur du rayon de la sphère précédente. Mais vu que nous travaillerons avec des vecteurs normalisés, cette longueur sera toujours égale à 1, ce qui donne au final :

Image utilisateur

Grâce aux angles Theta et Phi, nous pourrons calculer le vecteur orientation les doigts dans le nez. :-°

Et ces deux angles, comment va-t-on les calculer ?

Il n'y aura pas besoin des les calculer. Au niveau de la programmation, ces angles correspondront au mouvement de la souris. L'angle Phi représentera l'axe Y et Théta l'axe X.

En définitif, grâce à ces deux angles et aux coordonnées sphériques nous pourrons gérer l'orientation de notre caméra très facilement.

La gestion du déplacement

Avancer et reculer

La gestion du déplacement de la caméra va être différente de celle de l'orientation. Nous n'aurons ni besoin d'angles, ni de coordonnées sphériques, mais uniquement de vecteurs. On va diviser cette gestion en deux parties :

  • Le déplacement 'vertical' (à défaut de trouver meilleur terme) pour avancer et reculer

  • Le déplacement latéral pour aller vers la gauche ou la droite

On commence par la gestion du déplacement vertical car ce sera le plus facile. Pour faire avancer notre caméra nous n'aurons besoin que d'une simple addition. Oui oui vous avez bien entendu, nous n'aurons que d'une simple addition. :p

En effet, quand on se déplace vers l'avant (en marchant par exemple) on ne fait en réalité qu'une simple translation. Or quand on translate, on ne modifie pas notre orientation. C'est comme si l'on additionnait un vecteur par lui-même :

Image utilisateur

Pour faire avancer notre caméra, il suffira d’additionner le vecteur orientation par lui-même autant de fois que nécessaire. Et évidemment la faire pour reculer, il suffira de faire l'inverse d'une addition, c'est-à-dire ... une soustraction !

Image utilisateur

Je vous avais dit que la gestion verticale était simple. ^^

Le déplacement latéral (1/2)

Le déplacement latéral va être un peu plus délicat à gérer. Son principe est cependant simple : il faut trouver un vecteur 'perpendiculaire' (on dira plutôt orthogonal) au vecteur orientation :

Image utilisateur

Ce vecteur orthogonal va nous permettre de déplacer la caméra de façon latéral.
D'ailleurs, une fois que nous l'aurons trouvé il suffira de faire la même chose que pour avancer :

  • Pour aller vers la gauche : on additionnera ce vecteur par lui-même autant de fois que nécessaire

  • Pour aller vers la droite : on soustraira ce vecteur par lui-même autant de fois que nécessaire

Image utilisateur

Ah d'accord, on fait la même chose que pour avancer sauf qu'ici on avance 'perpendiculairement' à l'orientation ?

Vous avez compris. ^^
Il faut donc calculer ce vecteur pour avancer latéralement. Mais avant cela, il va falloir que l'on parle de la notion de normale d'un plan. Cette notion est importante car c'est elle qui va nous permettre de trouver ce fameux vecteur.

La normale d'un plan

Les normales sont très importantes avec OpenGL. On retrouvera cette notion plusieurs fois dans le tuto alors soyez attentifs. ;)

Une normale est une sorte de droite perpendiculaire à une surface plane, on appellera cette surface un plan. Si vous prenez un cahier (qui est une surface plane) la normale de son plan partira de la couverture pour aller vers le haut.

Image utilisateur

Cette normale peut être représentée par un vecteur. Et pour le calculer, il suffit de multiplier deux vecteurs appartenant à votre cahier :

Image utilisateur

Ça veut dire que si on multiplie deux vecteurs appartenant au cahier on trouve la normale du plan ?

Oui tout à fait, car il existe une propriété de la géométrie dans l'espace qui dit que : lorsque l'on multiplie deux vecteurs appartenant à un même plan, le résultat de cette multiplication sera un vecteur perpendiculaire au plan.

Ce vecteur résultat se nomme la normale du plan.

Cependant, pour pouvoir valider cette multiplication il va falloir respecter deux règles :

  • Premièrement, il faut absolument que les deux vecteurs à multiplier ne soient pas parallèles.

  • Deuxièmement, les vecteurs doivent partir vers des directions opposées. Il ne faut pas que leur flèche puissent se croiser.

Si une de ces deux règles n'est pas respectée alors votre calcul sera faux et vous ne trouverez pas la normale.
Cependant je vous rassure, nous utiliserons toujours de bons vecteurs, je ne vais pas être sadique et vous faire faire des calculs foireux. :p Mais sachez au moins que ces règles existent.

Si on reprend le schéma précédent, on remarque que les vecteurs V1 et V2ne sont pas parallèles et que leur flèche ne se rejoignent pas, les deux règles sont donc respectées. Si on multiplie ces deux vecteurs entre eux on trouvera la normale du plan. ;)

Bien évidemment, les normales ne se limitent pas aux surfaces carrées. D'ailleurs, on les utilisera presque toujours sur des triangles, ce qui compte c'est que la surface utilisée soit plane :

Image utilisateur

Petite question : comment est-ce qu'on multiplie deux vecteurs ?

Ce n'est pas à proprement parler une multiplication, il s'agit plus d'un produit vectoriel. Nous n'aurons pas à le faire nous même donc ne vous prenez pas la tête avec ça. ^^

D'ailleurs en parlant de ça, je vous avais également dit qu'il fallait faire attention à l'ordre dans la multiplication, que V1 x V2 n'était pas forcément égal à V2 x V1.

Cette remarque est toujours d'actualité. Si vous inversez la multiplication de deux vecteurs vous trouverez bien la normale, mais le problème c'est qu'elle sera inversée :

Image utilisateur

Faites juste attention à l'ordre des multiplications que nous ferrons et tout ira bien. ;)

Le déplacement latéral (2/2)

Pour en revenir à ce qui nous intéresse, je vous rappelle que l'on recherche un vecteur orthogonal au vecteur orientation pour que l'on puisse se déplacer latéralement.
Par chance, nous venons d'apprendre ce que sont les normales et je vous annonce que le vecteur que nous recherchons en est justement une !

Hein ? Mais pour calculer une normale il faut un plan non ? Or là on n'a que le vecteur orientation.

Oui tout à fait, nous avons besoin d'un plan dans lequel se trouve le vecteur orientation. Et pour le trouver, nous allons utiliser une petite astuce : l'axe y du repère 3D.

Cet axe formera le deuxième vecteur dont on a besoin pour la multiplication. En effet, quelle que soit l'orientation de la caméra, le vecteur orientation et le vecteur de l'axe y feront toujours partie du même plan :

Image utilisateur
Image utilisateur
Image utilisateur

Ces vecteurs font partie du même plan et sont orientés vers des directions différentes, on peut donc les utiliser pour calculer la normale du plan. :)

En définitif, pour trouver le vecteur de déplacement latéral il va falloir multiplier le vecteur orientation par le vecteur de l'axe y. Ensuite, il suffira de l'additionner ou de le soustraire par lui-même pour se déplacer vers la gauche ou vers la droite.

Pfiouuuu ... Tout ce gourbi pour calculer un vecteur !

Et oui c'est toute une histoire, mais au moins vous savez maintenant ce que sont les normales. C'est une notion hyper-importante qu'on aura l'occasion de retrouver lorsque l'on fera de l'éclaire dynamique avec les shaders. ;)

En résumé

Nous venons de voir pas mal de mathématique, je peux comprendre votre probable mal de tête. :p Je vais faire un résumé des points importants qu'il faut retenir.

Pour gérer l'orientation :

  • Nous avons besoin de deux angles : Theta et Phi

  • Ces deux angles permettent de calculer des coordonnées sphériques

  • Ces coordonnées sphériques formeront un vecteur qui représentera l'orientation de la caméra

Pour gérer le déplacement :

  • Si on additionne le vecteur orientation par lui-même alors la caméra avance. Quand le soustrait par lui-même, elle recule

  • Le vecteur orientation et le vecteur de l'axe y font partie du même plan

  • Si on multiplie ces vecteurs entre eux, on trouve la normale du plan. Cette normale sera le vecteur de déplacement latéral

Voilà ce qu'il faut retenir.

N'hésitez pas à relire encore et encore cette première partie de chapitre. ;)
Lorsque vous vous sentirez prêt, nous passerons à la suite du chapitre.

Implémentation de la caméra

La classe Camera

Le header

J'espère que vous avez eu le temps de digérer tout ce que l'on vient de voir. Si ça peut vous rassurer, sachez que c'était la partie la plus compliquée. Nous allons pouvoir passer à l'implémentation de la caméra. :)

Comme d'habitude, on va commencer par le header en déclarant une nouvelle classe : la classe Camera. Elle possèdera pas mal d'attributs, mais on les connait déjà tous. Voici le header de base :

#ifndef DEF_CAMERA
#define DEF_CAMERA

// Classe

class Camera
{
    public:

    Camera();
    ~Camera();


    private:
};

#endif

Dans la première partie de ce chapitre, nous avons vu que le déplacement de la caméra se divisait en 2 catégories : une qui s'occupait de gérer l'orientation et l'autre du déplacement pur et dur.

Pour gérer ce premier point, nous aurons besoin de 3 choses :

  • Un angle Theta : qui représentera l'orientation sur l'axe horizontal

  • Un angle Phi : représentera l'orientation sur l'axe vertical

  • Un vecteur orientation : qui représentera la direction dans laquelle on regarde

Ah mais comment on fait pour un avoir un vecteur en C++ ? Ça n'existe pas ?

En C++ non, mais n'oubliez pas que nous disposons d'une librairie mathématique complète. ;) GLM ne sert pas qu'aux matrices mais aussi aux vecteurs, quaternions, etc. Plein de choses en somme.

Il existe un objet que nous avons déjà utilisé et qui permet de gérer des vecteurs à 3 dimensions. Cet objet s'appelle vec3. Vous vous en souvenez j'espère . :p Il a l'avantage de posséder des méthodes simples qui permettent d'accéder à ses valeurs. Ainsi, pour retrouver sa coordonnée x par exemple, il nous suffit de faire :

monVecteur.x;

En définitif, nous allons utiliser un objet de type vec3 pour gérer l'orientation de la caméra. :)

Avec ça, nous avons donc nos 3 premiers attributs :

// Attributs d'orientation

float m_phi;
float m_theta;
glm::vec3 m_orientation;

Pour ne pas avoir d'erreur de compilation, il faut inclure les en-têtes relatifs à la librairie GLM :

#ifndef DEF_CAMERA
#define DEF_CAMERA


// Includes GLM

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>


// Classe

class Camera
{
    public:

    Camera();
    ~Camera();


    private:
};

#endif

Au niveau du deuxième point (le déplacement de la caméra), nous avons vu que nous aurons besoin de 3 choses :

  • Un vecteur orientation (le même que le précédent)

  • Un vecteur représentant l'axe vertical (l'axe y dans notre cas) : afin de pouvoir former un plan avec le vecteur orientation

  • Un vecteur de déplacement latéral : qui représente la normale du plan formé

Vu que nous avons déjà le vecteur orientation, on ne rajoute que les deux derniers vecteurs aux attributs :

// Attributs de déplacement

glm::vec3 m_axeVertical;
glm::vec3 m_deplacementLateral;

Petite info au passage : souvenez-vous que la méthode lookAt() prends 3 paramètres de type vec3. Pour les alimenter, il nous suffira juste de donner nos attributs qui sont eux-mêmes de objets de type vec3 ;)

En parlant de cette méthode, on va refaire un petit tour au niveau de son prototype :

mat4 lookAt(vec3 eye, vec3 center, vec3 up);
  • Vecteur eye : Position de la caméra

  • Vecteur center : Point fixé par la caméra

  • Vecteur up : Axe vertical utilisé (x, y ou z)

Si je vous remémore cette méthode c'est parce que nous en aurons besoin dans la classe Caméra. En effet, nous avons beau créer une classe toute neuve pour gérer la caméra, nous aurons toujours besoin d'utiliser les matrices et notamment la méthode lookAt() de la matrice modelview. Nous aurons donc besoin de 3 vecteurs correspondant aux 3 vecteurs demandés par cette méthode.

Comme on vient de le voir on a déjà l'axe vertical, on ne va donc ajouter que les deux autres dans les attributs. Cependant, on va faire une petite modification au niveau de leur nom de façon à les rendre plus compréhensibles. Ainsi, le vecteur eye deviendra le vecteur 'position' et center deviendra le vecteur 'pointCible' (point ciblé):

glm::vec3 m_position;
glm::vec3 m_pointCible;

Si on regroupe tous les attributs :

// Attributs

float m_phi;
float m_theta;
glm::vec3 m_orientation;

glm::vec3 m_axeVertical;
glm::vec3 m_deplacementLateral;

glm::vec3 m_position;
glm::vec3 m_pointCible;

Notre header est presque complet, il ne manque plus qu'à ajouter un constructeur - non pas un constructeur par défaut mais un constructeur qui prendra exactement les mêmes paramètres que la méthode lookAt(), à savoir :

  • Une position

  • Un point à cibler

  • Un vecteur représentant l'axe vertical

Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical);

Header final :

#ifndef DEF_CAMERA
#define DEF_CAMERA


// Includes GLM

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>


// Classe

class Camera
{
    public:

    Camera();
    Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical);
    ~Camera();


    private:

    float m_phi;
    float m_theta;
    glm::vec3 m_orientation;

    glm::vec3 m_axeVertical;
    glm::vec3 m_deplacementLateral;

    glm::vec3 m_position;
    glm::vec3 m_pointCible;
};

#endif

Les Constructeurs et le Destructeur

Vous commencez à avoir l'habitude avec les constructeurs et tout ça. :p Le principe ne change pas, on initialise toujours les attributs.

Au niveau du constructeur par défaut, il suffit simplement d'initialiser les angles avec la valeur 0 et les vecteurs avec leur constructeur par défaut.

Il faut cependant faire attention à l'attribut m_axeVertical. En effet, celui-ci permet savoir quel axe parmi X, Y ou Z représentera l'axe vertical. Si nous n'utilisons le constructeur par défaut il faudra quand même affecter une valeur non nulle sinon la caméra ne pourra pas faire ses calculs correctement. Vu que l'axe Z représente (malheureusement) souvent l'axe vertical alors nous donnerons cette valeur par défaut à l'attribut m_axeVertical.

Camera::Camera() : m_phi(0.0), m_theta(0.0), m_orientation(), m_axeVertical(0, 0, 1), m_deplacementLateral(), m_position(), m_pointCible()
{

}

Le second constructeur est identique au premier sauf que l'on initialise ici les vecteurs m_position, m_pointCible et m_axeVertical avec les paramètres fournis :

Camera::Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical) : m_phi(0.0), m_theta(0.0), m_orientation(), m_axeVertical(axeVertical), 
                                                                          m_deplacementLateral(), m_position(position), m_pointCible(pointCible)
{

}

En théorie, ce constructeur ne devrait pas être modifié mais nous allons quand même y apporter une modification temporaire. Je vais vous demander d'affecter une certaine valeur aux angles, je vous expliquerai pourquoi en dernière partie de chapitre (je ne vais pas alourdir les explications pour le moment :) ).

Ainsi, affectez la valeur -35.26 à l'angle Phi et -135 à l'angle Theta.

Camera::Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical) : m_phi(-35.26), m_theta(-135), m_orientation(), m_axeVertical(axeVertical), 
                                                                                  m_deplacementLateral(), m_position(position), m_pointCible(pointCible)
{

}

Pour finir, l'implémentation du destructeur se passe de commentaires. :p

Camera::~Camera()
{

}

La méthode orienter

Alimentation des angles

La méthode que nous allons coder va être la plus importante du chapitre, c'est grâce à elle que l'on va pouvoir calculer les coordonnées sphériques du vecteur orientation. Nous l'appellerons la méthode orienter(), elle prendra en paramètres 2 integer représentant les coordonnées relatives de la souris.

Voici le prototype de cette méthode :

void orienter(int xRel, int yRel);

Avant de commencer l'implémentation de cette méthode, nous allons revoir un petit peu les angles Phi et Theta. Je vous avais dit que ces angles allaient être alimentés par les mouvements de la souris :

  • L'angle Phi : représentant l'orientation sur l'axe vertical sera alimenté par la coordonnée y de la souris

  • L'angle Theta : représentant l'orientation sur l'axe horizontal sera alimentée par la coordonné x de la souris

A chaque tour de la boucle principale, nous devons modifier ces angles en fonction du mouvement de la souris.

Par exemple, si on bouge la souris vers le haut alors l'angle Phi s'agrandira. Au niveau du code, on additionne l'angle Phi par le petit mouvement vertical yRel :

void Camera::orienter(int xRel, int yRel)
{
    // Modification des angles

    m_phi += -yRel;
}

Hey tu t'es trompé non ? Tu as rajouté un signe - devant le paramètre yRel ?

Le sens trigonométrique

Non pas du tout. :p

C'est quelque chose que vous ne pouviez pas forcément savoir, mais lorsque l'on travaille avec des angles il y a un certain sens à respecter qui s'appelle le sens trigonométrique (sens inverse des aiguilles d'une montre). Ce sens étant inversé, les angles se retrouvent donc eux-aussi inversés.

Ainsi, un mouvement de souris allant vers le haut (donc une augmentation de l'angle Phi) sera interprété par une augmentation de son opposé (donc -Phi). Même chose pour les mouvements horizontaux, l'angle Theta sera inversé.

Mais avant on a jamais inversé nos angles ?

Dans la plupart des cas, nous n'aurons pas à inverser nos angles. Cependant ici, il y a un contre-sens entre le sens trigonométrique et le sens normal de la souris.
Si vous bougez votre souris vers le haut alors sa position Y va augmenter, elle utilise (grossièrement parlant) le sens horaire.
Or un angle, lui, va utiliser le sens anti-horaire. Donc pour compenser cette inversion, il faut additionner l'opposé de l'angle.

D'ailleurs en parlant de ça, vous avez probablement remarqué que dans certains jeux, on vous offrait la possibilité d'inverser le sens des axes X et Y. Cette option spécifie simplement à votre caméra s'il faut utiliser le sens horaire ou le sens trigonométrique, donc additionner l'angle normal ou son opposé. Si vous avez envie d'implémenter la même option un jour vous savez maintenant comment faire. ;)

En définitif, nous devons additionner l'opposé des mouvements générés par la souris à cause du sens trigonométrique.

L'angle Theta ne fait d'ailleurs pas exception :

void Camera::orienter(int xRel, int yRel)
{
    // Modification des angles

    m_phi += -yRel;
    m_theta += -xRel;
}

Nous allons rajouter encore une toute petite chose à ces calculs (ne vous inquiétez pas, il n'y a a rien de tordu cette fois :p ). Vous verrez tout à l'heure que si nous utilisons les coordonnées relatives directement comme ça, la caméra va bouger trop vite et nous fera des mouvements bizarres. Pour régler ce problème, on va abaisser les coordonnées relatives en les multipliant par 0.5. De cette façon, les mouvements de la caméra seront deux fois moins rapides, ce qui sera utile pour mieux voir. ^^

Dans la dernière partie, nous rajouterons un attribut pour gérer cette vitesse avec un setter :

void Camera::orienter(int xRel, int yRel)
{
    // Modification des angles

    m_phi += -yRel * 0.5;
    m_theta += -xRel * 0.5;
}
Calcul des coordonnées sphériques

Nous avons maintenant des angles actualisés en fonction de la souris, c'est bien mais il faut encore imposer une limite à un angle en particulier. En effet, dans la première partie du chapitre je vous avais dit que le vecteur orientation ne devait jamais être parallèle avec l'axe Y vous vous souvenez ? Car dans ce cas, nous ne pouvions plus utiliser la formule des coordonnées sphériques.

Pour éviter de se retrouver devant cette alignement, il faut limiter l'angle Phi à une valeur maximale de 89° (ou -89° dans l'autre sens) sinon l'une des deux règles sur le calcul de la normale sera violée. Donc en imposant cette limite, nous pourrons toujours utiliser le plan formé par les deux vecteurs pour le calcul du déplacement latéral. ;)

On implémente donc deux conditions pour limiter l'attribut m_phi à une valeur de 89° ou -89° :

void Camera::orienter(int xRel, int yRel)
{
    // Récupération des angles

    m_phi += -yRel * 0.5;
    m_theta += -xRel * 0.5;


    // Limitation de l'angle phi

    if(m_phi > 89.0)
        m_phi = 89.0;

    else if(m_phi < -89.0)
        m_phi = -89.0;
}

Nous avons maintenant des angles parfaitement utilisables, nous allons pouvoir passer au calcul des coordonnées sphériques de l'orientation. Et pour cela on va utiliser une formule que nous avons déjà vue. :p

Image utilisateur

Nous devons donc transposer cette formule en code source. On utilisera le setter tout bête de chaque coordonnée du vecteur m_orientation pour affecter le résultat :

// Calcul des coordonnées sphériques

m_orientation.x = cos(m_phi) * sin(m_theta);
m_orientation.y = sin(m_phi);
m_orientation.z = cos(m_phi) * cos(m_theta);

....

....

Vous ne voyez pas une petite erreur dans ce code ?

Euh non je pense pas ... Il y en a une ?

Hum bon c'est un peu sadique je l'avoue, mais je veux que vous intégrez bien cette notion : le problème ici c'est qu'on envoie des angles exprimés en degrés alors que les fonctions sin() et cos() attendent des angles exprimés radians. Si on ne modifie pas ça, on risque d'avoir une sacrée surprise au moment de déplacer la caméra. :lol:

Pour corriger le problème, nous allons convertir les angles Phi et Theta en radian. On stockera les nouveaux angles dans des variables temporaires que l'on utilisera pour calculer les coordonnées sphériques.

Je vous rappelle que pour convertir un angle en radian il faut le multiplier par Pi puis le diviser par 180 :

// Conversion des angles en radian

float phiRadian = m_phi * M_PI / 180;
float thetaRadian = m_theta * M_PI / 180;


// Calcul des coordonnées sphériques

m_orientation.x = cos(phiRadian) * sin(thetaRadian);
m_orientation.y = sin(phiRadian);
m_orientation.z = cos(phiRadian) * cos(thetaRadian);

Cette fois le calcul est fonctionnel car nous utilisons bien des angles exprimés en radians. Cependant et malgré ça, il subsiste encore un petit problème.

En effet, la formule avec laquelle je vous rabâche la tête depuis tout à l'heure n'est valable que si l'on utilise l'axe Y. Or tout le monde n'utilise pas forcément cet axe, il faut gérer les axes X et Z également.

Pour cela, il n'y a rien de compliqué puisqu'il suffit juste d'interchanger les setters du vecteur orientation entre eux. Je vous donne les formules pour les 3 axes X, Y et Z :

Image utilisateur
Image utilisateur

(Celle-la on la connait)

Image utilisateur

Pour gérer ces 3 formules nous allons simplement utiliser 3 blocs if() qui vont tester les coordonnées de l'attribut m_axeVertical. Celui qui posséde la valeur 1.0 représentera l'axe vertical :

// Si l'axe vertical est l'axe X

if(m_axeVertical.x == 1.0)
{
}


// Si c'est l'axe Y

else if(m_axeVertical.y == 1.0)
{
}


// Sinon c'est l'axe Z

else
{
}

Si le développeur n'a pas rentré la valeur 1.0 à une coordonnée alors ce sera l'axe Z qui sera choisi. Le développeur doit faire attention à la valeur qu'il donne pour ce vecteur, c'est sa responsabilité. ;)

Une fois les blocs créés, il suffit d'associer la bonne formule aux bonnes coordonnées :

// Si l'axe vertical est l'axe X

if(m_axeVertical.x == 1.0)
{
    // Calcul des coordonnées sphériques

    m_orientation.x = sin(phiRadian);
    m_orientation.y = cos(phiRadian) * cos(thetaRadian);
    m_orientation.z = cos(phiRadian) * sin(thetaRadian);
}


// Si c'est l'axe Y

else if(m_axeVertical.y == 1.0)
{
    // Calcul des coordonnées sphériques

    m_orientation.x = cos(phiRadian) * sin(thetaRadian);
    m_orientation.y = sin(phiRadian);
    m_orientation.z = cos(phiRadian) * cos(thetaRadian);
}


// Sinon c'est l'axe Z

else
{
    // Calcul des coordonnées sphériques

    m_orientation.x = cos(phiRadian) * cos(thetaRadian);
    m_orientation.y = cos(phiRadian) * sin(thetaRadian);
    m_orientation.z = sin(phiRadian);
}

Cette fois, le code est totalement fonctionnel. :D

Dernier point à préciser, on devrait en théorie normaliser le vecteur orientation, mais vu que nous utilisons des fonctions trigonométriques le vecteur est déjà normalisé. En effet, lorsqu'on utilise la trigonométrie, le vecteur qui représente le rayon a toujours une norme égale à 1 donc il est normalisé. ;)

Calcul du vecteur de déplacement latéral

Nous avons maintenant le fameux vecteur orientation, nous pouvons donc l'utiliser pour le calcul du vecteur de déplacement latéral.
Vous vous souvenez que pour le trouver, il fallait multiplier le vecteur orientation par le vecteur représentant l'axe vertical. On trouve ainsi la normale du plan formé par ces deux vecteurs.

Quand je parle de la multiplication c'est un peu un abus de langage car il s'agit en fait d'un produit vectoriel (en comparaison du produit d'une multiplication). Le produit vectoriel permet de trouver le fameux vecteur orthogonal que nous cherchons.

Ce produit ne se fait pas avec un signe * classique, il faut plutôt utiliser une méthode de la librairie GLM qui s'appelle cross() :

glm::vec3 cross(glm::vec3 vector1, glm::vec3 vector2);

Cette méthode retourne le vecteur orthogonal des deux paramètres donnés.

Nous l'appelons donc en lui donnant les attributs m_axeVertical et m_orientation pour qu'elle puisse le calculer :

// Calcul de la normale

m_deplacementLateral = cross(m_axeVertical, m_orientation);

On pensera cette fois à normaliser le résultat car on n'utilise pas la trigo, ce qui fait que rien n'est normalisé tant qu'on ne l'a pas demandé. Mais encore une fois, la librairie GLM nous fournit directement une méthode pour normaliser les vecteurs. Nous n'avons rien à faire décidément. :p

Cette méthode s'appelle normalize() :

glm::vec3 normalize(glm::vec3 vector);

Elle prend en paramètre le vecteur à normaliser et renvoie le résultat.

Nous l'appellerons en lui donnant le vecteur que l'on vient de calculer, à savoir m_deplacementVertical :

// Calcul de la normale

m_deplacementLateral = cross(m_axeVertical, m_orientation);
m_deplacementLateral = normalize(m_deplacementLateral);
Actualisation du point ciblé

Enfin pour terminer cette méthode, il ne reste plus qu'à actualiser le point fixé par la matrice modelview (l'un des trois paramètres dont à besoin la matrice).

Pour trouver ce point, on va utiliser une petite astuce : on va additionner le vecteur position avec le vecteur orientation. Le résultat sera le point qu'attend la matrice, soit le point pile en face de la position de la caméra.

On peut additionner deux objets vec3 ? Ça doit être compliqué non ?

Non pas du tout car les développeurs de GLM ont pensé à tout et ont intégré, au même titre que les matrices, les surcharges d'opérateurs. Nous pouvons donc les additionner sans problème juste en utilisant le signe +. Génial non ? ^^

Pour additionner le vecteur position et orientation, il nous suffit donc d'utiliser le signe + :

Au niveau du code, on additionne simplement ces deux vecteurs :

// Calcul du point ciblé pour OpenGL

m_pointCible = m_position + m_orientation;

Addition terminée. :p

Et ce vecteur on ne le normalise pas ?

Nan, les vecteurs position et pointCible ne doivent jamais être normalisés car ils représentent des positions dans l'espace. Ils permettent à OpenGL de savoir où l'on se trouve dans un monde 3D, et un monde 3D ne se limite pas à des positions de 1 unité maximum.

Les vecteurs normalisés servent pour les calculs diverses comme l'orienation et le vecteur déplacementLateral.

Récapitulation

Si on récapitule tout ce que l'on vient de coder :

void Camera::orienter(int xRel, int yRel)
{
    // Récupération des angles

    m_phi += -yRel * 0.5;
    m_theta += -xRel * 0.5;


    // Limitation de l'angle phi

    if(m_phi > 89.0)
        m_phi = 89.0;

    else if(m_phi < -89.0)
        m_phi = -89.0;


    // Conversion des angles en radian

    float phiRadian = m_phi * M_PI / 180;
    float thetaRadian = m_theta * M_PI / 180;


    // Si l'axe vertical est l'axe X

    if(m_axeVertical.x == 1.0)
    {
        // Calcul des coordonnées sphériques

        m_orientation.x = sin(phiRadian);
        m_orientation.y = cos(phiRadian) * cos(thetaRadian);
        m_orientation.z = cos(phiRadian) * sin(thetaRadian);
    }


    // Si c'est l'axe Y

    else if(m_axeVertical.y == 1.0)
    {
        // Calcul des coordonnées sphériques

        m_orientation.x = cos(phiRadian) * sin(thetaRadian);
        m_orientation.y = sin(phiRadian);
        m_orientation.z = cos(phiRadian) * cos(thetaRadian);
    }


    // Sinon c'est l'axe Z

    else
    {
        // Calcul des coordonnées sphériques

        m_orientation.x = cos(phiRadian) * cos(thetaRadian);
        m_orientation.y = cos(phiRadian) * sin(thetaRadian);
        m_orientation.z = sin(phiRadian);
    }


    // Calcul de la normale

    m_deplacementLateral = cross(m_axeVertical, m_orientation);
    m_deplacementLateral = normalize(m_deplacementLateral);


    // Calcul du point ciblé pour OpenGL

    m_pointCible = m_position + m_orientation;
}

Grâce à cette méthode, nous pouvons calculer non seulement l'orientation de la caméra mais aussi son vecteur orthogonal qui sera utilisé pour le déplacement latéral. :)

La méthode deplacer

La méthode suivante va permettre de s'occuper du déplacement pur et dur de la caméra. Nous savons déjà comment faire en plus ça ne sera pas compliqué. ^^

Nous appellerons cette méthode la méthode deplacer(), elle prendra en paramètre une référence constante sur un objet de type Input. Car oui, nous aurons besoin de savoir si les touches de déplacement sont pressées ou non, nous avons donc besoin d'un objet de ce type.

void deplacer(Input const &input);

Pour faire avancer ou reculer la caméra on additionnera ou on soustraira le vecteur orientation :

Image utilisateur
Image utilisateur

Et pour la déplacer latéralement on additionnera ou on soustraira le vecteur orthogonal au vecteur orientation :

Image utilisateur

Au niveau du code, on va encapsuler les quatre touches de déplacement dans des blocs if. Si une touche est pressée, alors on effectue l'action désirée (addition ou soustraction de vecteurs).

Par exemple, si la touche Haut (SDL_SCANCODE_UP) est pressée alors on additionnera le vecteur orientation avec le vecteur position pour faire avancer la caméra. On ajoutera ici aussi une contrainte de vitesse exactement comme pour l'orientation de la caméra (on créera également un attribut pour gérer ça plus tard). Nous diviserons la vitesse de déplacement par 2 en multipliant le vecteur orientation par 0.5 (un vecteur multiplié par un nombre est différent d'un calcul vectoriel. On multiplie juste les coordonnées par 0.5 dans ce cas ^^ ) :

void Camera::deplacer(Input const &input)
{
    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
        m_position = m_position + m_orientation * 0.5f;
}

Lorsque la position de la caméra est modifiée alors le point ciblé par OpenGL doit également être modifié. Pour cela, on fait exactement ce que l'on a fait dans la méthode orienter() :

void Camera::deplacer(Input const &input)
{
    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
    {
        m_position = m_position + m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }
}

Grâce à cette condition, notre caméra peut avancer quand nous voulons. :D

D'ailleurs pour la faire reculer ce n'est pas plus compliqué, on fait exactement la même chose sauf que cette fois on soustrait le vecteur position et le vecteur orientation. On utilisera le scancode SDL_SCANCODE_DOWN pour la touche du clavier :

// Recul de la caméra

if(input.getTouche(SDL_SCANCODE_DOWN))
{
    m_position = m_position - m_orientation * 0.5f;
    m_pointCible = m_position + m_orientation;
}

Le principe ne change pas pour le déplacement latéral : si on veut aller vers la gauche on additionne le vecteur m_deplacementLateral à la position, et si on veut aller vers la doite on le soustrait. On utilisera respectivement les scancodes SDL_SCANCODE_LEFT et SDL_SCANCODE_RIGHT :

// Déplacement vers la gauche

if(input.getTouche(SDL_SCANCODE_LEFT))
{
    m_position = m_position + m_deplacementLateral * 0.5f;
    m_pointCible = m_position + m_orientation;
}


// Déplacement vers la droite

if(input.getTouche(SDL_SCANCODE_RIGHT))
{
    m_position = m_position - m_deplacementLateral * 0.5f;
    m_pointCible = m_position + m_orientation;
}

Si on récapitule tout ça :

void Camera::deplacer(Input const &input)
{
    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
    {
        m_position = m_position + m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Recul de la caméra

    if(input.getTouche(SDL_SCANCODE_DOWN))
    {
        m_position = m_position - m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la gauche

    if(input.getTouche(SDL_SCANCODE_LEFT))
    {
        m_position = m_position + m_deplacementLateral * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la droite

    if(input.getTouche(SDL_SCANCODE_RIGHT))
    {
        m_position = m_position - m_deplacementLateral * 0.5f;
        m_pointCible = m_position + m_orientation;
    }
}

Petite question : pourquoi n'utilise-t-on pas des else if ?

Simplement pour être capable d'utiliser plusieurs touches à la fois. ;) Si on utilisait des blocs else if, il n'y aurait qu'une seule touche qui serait gérée pour se déplacer.

Il ne manque plus qu'une petite chose à cette méthode. En effet, vu qu'on a l'objet input sous la main on peut en profiter pour savoir si il y a eu un mouvement de souris. Et s'il y a mouvement de souris alors il y a une modification de l'orientation et donc un appel à la méthode orienter(). ^^

On va donc rajouter une condition qui sera déclenchée lors d'un mouvement de la souris. A l'intérieur, on appellera la méthode orienter() à laquelle on donnera en paramètres les attributs m_xRel et m_yRel de l'objet input.

// Gestion de l'orientation

if(input.mouvementSouris())
    orienter(input.getXRel(), input.getYRel());

Méthode finale :

void Camera::deplacer(Input const &input)
{
    // Gestion de l'orientation

    if(input.mouvementSouris())
        orienter(input.getXRel(), input.getYRel());


    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
    {
        m_position = m_position + m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Recul de la caméra

    if(input.getTouche(SDL_SCANCODE_DOWN))
    {
        m_position = m_position - m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la gauche

    if(input.getTouche(SDL_SCANCODE_LEFT))
    {
        m_position = m_position + m_deplacementLateral * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la droite

    if(input.getTouche(SDL_SCANCODE_RIGHT))
    {
        m_position = m_position - m_deplacementLateral * 0.5f;
        m_pointCible = m_position + m_orientation;
    }
}

Avec cette méthode, nous pouvons gérer complétement le déplacement de la caméra. :D

Méthode lookAt

On arrive à la dernière méthode de notre caméra. :(

Jusqu'à maintenant, nous avons utilisé pas mal de vecteurs mais vous savez que seuls 3 d'entre eux sont vraiment importants : la position, le point ciblé et l'axe vertical. Ces trois vecteurs sont indispensables pour la matrice modelview, nous devons donc les lui envoyer.

Nous allons coder une nouvelle méthode pour s'occuper de ça, on l'appellera lookAt() en référence à la méthode du même nom chez GLM. Elle prendra en paramètre une référence (non constante) sur la matrice modelview :

void lookAt(glm::mat4 &modelview);

Pour son implémentation, on appelle simplement la méthode lookAt() de la matrice modelview, on lui donne au passage les 3 vecteurs qu'elle demande. Faites attention à utiliser le namespace glm:: ici même si vous utilisez le using dans votre fichier. Vu que nous avons deux méthodes portant le même nom, il faut utiliser le namespace pour les différencier :

void Camera::lookAt(glm::mat4 &modelview)
{
    // Actualisation de la vue dans la matrice

    modelview = glm::lookAt(m_position, m_pointCible, m_axeVertical);
}

Implémentation de la caméra

Notre caméra est maintenant totalement opérationnelle ! Il ne manque plus qu'à l'implémenter dans notre scène. :D

On ajoute donc l'en-tête Camera.h dans le header SceneOpenGL.h, puis on déclare un objet de type Camera dans la méthode bouclePrincipale(). On placera cette caméra au point de coordonnées (3, 3, 3) et elle ciblera le point de coordonnées (0, 0, 0). Bien entendu, l'axe vertical reste l'axe y (0, 1, 0).

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    Uint32 frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Caméra mobile

    Camera camera(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));


    ....
}

On en profite au passage pour piéger et cacher le pointeur de la souris grâce aux méthodes suivantes :

// Capture du pointeur

m_input.afficherPointeur(false);
m_input.capturerPointeur(true);
void SceneOpenGL::bouclePrincipale()
{
    // Variables

    Uint32 frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Caméra mobile

    Camera camera(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));
    m_input.afficherPointeur(false);
    m_input.capturerPointeur(true);


    ....
}

Ensuite, on appelle la méthode deplacer() de l'objet camera sans oublier de donner l'attribut m_input pour qu'il puisse travailler avec :

// Boucle principale

while(!m_input.terminer())
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
        break;

    camera.deplacer(m_input);


    ....
}

Enfin, on remplace la méthode lookAt() de la matrice modelview par la méthode lookAt() de la caméra. On n'oublie pas de lui donner le paramètre qu'elle attend soit la matrice modelview elle-même :

....


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


// Gestion de la caméra

camera.lookAt(modelview);


....

Et là, vous pouvez compiler joyeusement votre code source. :D

Image utilisateur

Essayez votre nouvelle caméra, vous pouvez vous déplacer dans toutes les directions sans aucun problème. ^^

Notre caméra est maintenant quasi-complète, il manque en réalité encore une méthode. En effet, tout à l'heure je vous ai demandé de mettre des valeurs par défaut pour les angles dans le constructeur. Or cette solution ne fonctionne que dans le cas où la caméra se trouve au point de coordonnées (3, 3, 3). Dans la dernière partie de ce chapitre, nous allons coder une nouvelle méthode qui permettra de régler ce problème.

Fixer un point

Dans ces deux dernières parties, nous allons ajouter quelques petites fonctionnalités à la classe Camera. Nous nous occuperons :

  • D'ajouter une méthode pour cibler un point

  • D'ajouter un setter pour la position de la caméra

  • D'ajouter deux attributs pour gérer sa sensibilité (orientation) et sa vitesse (déplacement)

Nous commencerons évidemment par le premier point. ;) Les autres seront développés dans la dernière partie.

Fonctionnalités de la classe Camera

La méthode setPointcible()

Tout à l'heure, je vous avais demandé d'affecter certaines valeurs aux angles Theta et Phi dans le constructeur. Si vous n'aviez pas fait cela, vos angles auraient été initialisés avec la valeur 0 et votre regard aurait été complétement inversé par rapport au point à cibler :

Image utilisateur

Vous auriez bien placé votre caméra mais elle ne serait pas du tout orientée de la bonne façon malgré le fait que vous auriez donné en paramètre le point à cibler. Elle serait orientée selon la valeur des angles soit la valeur 0 à cause du constructeur. Rappelez-vous qu'à chaque fois on ne fait qu'additionner les changements de position et les angles partent avec leur valeur d'origine :

// Modification des angles

m_phi += -yRel * 0.5;
m_theta += -xRel * 0.5;

Pour régler ce problème, il faut que les angles soient initialisés avec la bonne orientation dès le départ. Et pour ça, nous allons coder une méthode qui fera l'inverse de la méthode orienter(). Elle devra être capable de calculer la valeur initiale des angles à partir des vecteurs qu'on lui envoie.

Cette méthode va être facile à coder, il suffit juste de reprendre la formule des coordonnées sphériques et d'en inverser les opérations pour trouver la valeur des angles. :p On commence avec la formule valable pour l'axe Y :

Image utilisateur
Image utilisateur
Image utilisateur
Image utilisateur

Avec cette formule, on peut trouver les angles Theta et Phi à partir des coordonnées (x, y, z) - coordonnées qui appartiennent au vecteur orientation dans le cas où on utilise l'axe Y. Pour trouver les angles avec les autres axes, il suffit juste d'interchanger les formules de départ pour tomber sur les résultats suivants :

Image utilisateur
Image utilisateur

Vous voyez que la formule reste la même, il n'y a que la coordonnée utilisée qui va changer. ^^

Oui c'est bien tout ça mais on a besoin des coordonnées du vecteur orientation pour calculer les angles non ? Or on ne l'a pas celui-la à l'initialisation de la caméra ?

Oui tout à fait nous ne l'avons pas à l'initialisation de la caméra. Mais encore une fois, nous pouvons le trouver en inversant une autre formule mathématique.

Et oui, souvenez-vous que pour trouver le vecteur pointCible, on additionne le vecteur position et le vecteur orientation ? :

// Calcul du point ciblé pour OpenGL

m_pointCible = m_position + m_orientation;
Image utilisateur

Donc pour trouver le vecteur orientation (et surtout ses coordonnées), il suffit de soustraire le vecteur pointCible par le vecteur position :

Image utilisateur

Grâce à cette soustraction, on va pouvoir calculer la valeur initiale des angles au moment de l'initialisation de la caméra. ^^

Pour effectuer tous ces calculs, nous utiliserons une méthode, ou plutôt un setter sur l'attribut m_pointCible que nous appellerons simplement setPointcible(). Il prendra en paramètre le vecteur à cibler :

void setPointcible(glm::vec3 pointCible);

On commence son implémentation en soustrayant les vecteurs pointCible et position pour trouver le vecteur orientation. Comme d'habitude, on normalisera ce vecteur :

void Camera::setPointcible(glm::vec3 pointCible)
{
    // Calcul du vecteur orientation

    m_orientation = m_pointCible - m_position;
    m_orientation = normalize(m_orientation);
}

Une fois le vecteur orientation trouvé, on peut maintenant calculer les angles Phi et Theta grâce aux formules inversées que l'on a trouvées précédemment :

Image utilisateur
Image utilisateur
Image utilisateur

Comme pour la méthode orienter(), on commence par faire 3 blocs if qui vont permettre de gérer le cas des trois axes verticaux :

// Si l'axe vertical est l'axe X

if(m_axeVertical.x == 1.0)
{
}


// Si c'est l'axe Y

else if(m_axeVertical.y == 1.0)
{
}


// Sinon c'est l'axe Z

else
{
}

Une fois que c'est fait, on associe la bonne formule au bon axe. Je vous donne l'exemple du cas où on utilise l'axe Y :

// Calcul des angles pour l'axe Y

m_phi = asin(m_orientation.y);
m_theta = acos(m_orientation.z / cos(m_phi));

Implémentation du bon calcul dans les blocs if :

// Si l'axe vertical est l'axe X

if(m_axeVertical.x == 1.0)
{
    // Calcul des angles

    m_phi = asin(m_orientation.x);
    m_theta = acos(m_orientation.y / cos(m_phi));
}


// Si c'est l'axe Y

else if(m_axeVertical.y == 1.0)
{
    // Calcul des angles

    m_phi = asin(m_orientation.y);
    m_theta = acos(m_orientation.z / cos(m_phi));
}


// Sinon c'est l'axe Z

else
{
    // Calcul des angles

    m_phi = asin(m_orientation.x);
    m_theta = acos(m_orientation.z / cos(m_phi));
}

En temps normal nous pourrions nous arrêter là mais il existe un petit problème avec la coordonnée utilisée pour calculer l'angle Theta. En effet, les formules que nous utilisons ne sont valables que si cette coordonnée est supérieure à 0. Si elle est inférieure à 0, les formules vont donner une valeur opposée de l'angle ( -Theta ), ce qui inversera le résultat final.

Pour régler ce problème, il suffit d'ajouter une sous-condition dans chaque bloc if pour vérifier le signe de la coordonnée utilisée : s'il est négatif, alors on multiplie l'angle Theta par -1 pour avoir l'opposée de l'opposée, et donc la vraie valeur. S'il est positif alors l'angle est déjà correcte, on ne fait rien.

Voici ce que ça donne dans le cas où on utilise l'axe Y :

// Calcul des angles

m_phi = asin(m_orientation.y);
m_theta = acos(m_orientation.z / cos(m_phi));

if(m_orientation.z < 0)
    m_theta *= -1;

Implémentation pour tous les blocs if :

// Si l'axe vertical est l'axe X

if(m_axeVertical.x == 1.0)
{
    // Calcul des angles

    m_phi = asin(m_orientation.x);
    m_theta = acos(m_orientation.y / cos(m_phi));

    if(m_orientation.y < 0)
        m_theta *= -1;
}


// Si c'est l'axe Y

else if(m_axeVertical.y == 1.0)
{
    // Calcul des angles

    m_phi = asin(m_orientation.y);
    m_theta = acos(m_orientation.z / cos(m_phi));

    if(m_orientation.z < 0)
        m_theta *= -1;
}


// Sinon c'est l'axe Z

else
{
    // Calcul des angles

    m_phi = asin(m_orientation.x);
    m_theta = acos(m_orientation.z / cos(m_phi));

    if(m_orientation.z < 0)
        m_theta *= -1;
}

On arrive à la fin de la méthode, il ne reste plus qu'une chose à faire : convertir les angles Theta et Phi en degrés pour pouvoir les utiliser avec les coordonnées relatives de la souris. Les fonctions cos() et sin() ne prennent et ne renvoient que des angles exprimés en radians (je ne l'ai pas déjà dit :-° ) alors que nous, nous avons besoin d'angles exprimés en degrés pour pouvoir travailler avec la souris. Il faut donc convertir les résultats en multipliant les angles par 180 puis en les divisant par Pi :

// Conversion en degrés

m_phi = m_phi * 180 / M_PI;
m_theta = m_theta * 180 / M_PI;

Si on résume tout ça :

void Camera::setPointcible(glm::vec3 pointCible)
{
    // Calcul du vecteur orientation

    m_orientation = m_pointCible - m_position;
    m_orientation = normalize(m_orientation);


    // Si l'axe vertical est l'axe X

    if(m_axeVertical.x == 1.0)
    {
        // Calcul des angles

        m_phi = asin(m_orientation.x);
        m_theta = acos(m_orientation.y / cos(m_phi));

        if(m_orientation.y < 0)
            m_theta *= -1;
    }


    // Si c'est l'axe Y

    else if(m_axeVertical.y == 1.0)
    {
        // Calcul des angles

        m_phi = asin(m_orientation.y);
        m_theta = acos(m_orientation.z / cos(m_phi));

        if(m_orientation.z < 0)
            m_theta *= -1;
    }


    // Sinon c'est l'axe Z

    else
    {
        // Calcul des angles

        m_phi = asin(m_orientation.x);
        m_theta = acos(m_orientation.z / cos(m_phi));

        if(m_orientation.z < 0)
            m_theta *= -1;
    }


    // Conversion en degrés

    m_phi = m_phi * 180 / M_PI;
    m_theta = m_theta * 180 / M_PI;
}

Et voilà ! Grâce à cette méthode nous pouvons enfin régler le problème d'initialisation de la caméra. Et de plus, nous pouvons l'orienter quand on le veut (pour une cinématique par exemple). ^^

Implémentons sans plus tarder cette méthode dans le constructeur. Pour ça, on ré-initialise les angles Theta et Phi à 0 puis on appelle la méthode setPointCible() :

Camera::Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical) : m_phi(0.0), m_theta(0.0), m_orientation(),
                                                                                  m_axeVertical(axeVertical), m_deplacementLateral(),
                                                                                  m_position(position), m_pointCible(pointCible)
{
    // Actualisation du point ciblé

    setPointcible(pointCible);
}

Améliorations

Dans cette dernière partie, nous allons finir en douceur et voir les dernières petites fonctionnalités que nous allons ajouter à notre classe Camera. Je vous les rappelle :

  • Ajouter un setter pour la position de la caméra

  • Ajouter deux attributs pour gérer sa sensibilité (orientation) et sa vitesse (déplacement)

On commencera par le premier point. :)

La méthode setPosition()

Bon je pense que vous avez l'habitude des setters maintenant, surtout que celui-ci va être plus simple que le précédent. :p

Nous allons coder un petit setter setPosition() qui nous permettra de positioner la caméra quand on le souhaitera :

void setPosition(glm::vec3 position);

Ce setter permet de mettre à jour l'attribut m_position. On utilisera le signe = qui nous permet de copier deux objets de type vec3 facilement. Le seul point auquel il faut faire attention est le fait qu'il faut mettre à jour le point ciblé à chaque fois que l'on change de position :

void Camera::setPosition(glm::vec3 position)
{
    // Mise à jour de la position

    m_position = position;


    // Actualisation du point ciblé

    m_pointCible = m_position + m_orientation;
}

Bien entendu on ne normalise pas ce vecteur-la, il fait partie des deux exceptions. ;)

Les attributs sensibilité et rapidité

On passe à la fonctionnalité suivante, nous allons créer deux nouveaux paramètres qui vont nous permettre de modifier la vitesse de déplacement de la caméra.

Vous vous souvenez que je vous avais demandé de multiplier les angles et le vecteur orientation par 0.5 pour réduire la vitesse de la caméra ?

void Camera::orienter(int xRel, int yRel)
{
    // Récupération des angles

    m_phi += -yRel * 0.5f;
    m_theta += -xRel * 0.5f;


    .....
}
void Camera::deplacer(Input const &input)
{
    .....


    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
    {
        m_position = m_position + m_orientation * 0.5f;
        m_pointCible = m_position + m_orientation;
    }


    .....
}

Le problème avec ces valeurs c'est qu'elles sont figées, on ne peut pas les moduler. Tous les jeux-vidéo n'utilisent pas forcément la même vitesse de déplacement et de plus, les valeurs que nous avons mises sont dignes des plus grands championnats de courses d'escargots !

En effet, pour un véritable jeu il faut multiplier ces vitesses par 10 voire plus et non 0.5.

Pour gérer tout ça, nous allons implémenter deux attributs m_sensibilite et m_vitesse qui correspondront respectivement à la sensibilité de la souris (l'orientation) et à la vitesse de déplacement (la position) :

class Camera
{
    public:

    // Méthodes

    ....


    private:

    float m_phi;
    float m_theta;
    glm::vec3 m_orientation;

    glm::vec3 m_axeVertical;
    glm::vec3 m_deplacementLateral;

    glm::vec3 m_position;
    glm::vec3 m_pointCible;

    float m_sensibilite;
    float m_vitesse;
};

Bien entendu, on modifie nos deux constructeurs pour qu'ils puissent prendre en paramètre des valeurs pour ces deux attributs.

Voici le constructeur par défaut :

Camera::Camera() : m_phi(0.0), m_theta(0.0), m_orientation(), m_axeVertical(0, 0, 1), m_deplacementLateral(), m_position(), m_pointCible(), m_sensibilite(0.0), m_vitesse(0.0)
{

}

Et voici le second constructeur :

Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical, float sensibilite, float vitesse);
Camera::Camera(glm::vec3 position, glm::vec3 pointCible, glm::vec3 axeVertical, float sensibilite, float vitesse) : m_phi(0.0), m_theta(0.0), m_orientation(), 
                                                                                                                    m_axeVertical(axeVertical), m_deplacementLateral(),
                                                                                                                    m_position(position), m_pointCible(pointCible),
                                                                                                                    m_sensibilite(sensibilite), m_vitesse(vitesse)
{
    // Actualisation du point ciblé

    setPointcible(pointCible);
}

Ensuite, on inclut l'attribut m_sensibilite dans la méthode orienter() :

void Camera::orienter(int xRel, int yRel)
{
    // Modification des angles

    m_phi += -yRel * m_sensibilite;
    m_theta += -xRel * m_sensibilite;


    .....
}

Et enfin, on ajoute l'attribut m_vitesse à chaque déplacement possible de la caméra :

void Camera::deplacer(Input const &input)
{
    // Gestion de l'orientation

    if(input.mouvementSouris())
        orienter(input.getXRel(), input.getYRel());


    // Avancée de la caméra

    if(input.getTouche(SDL_SCANCODE_UP))
    {
        m_position = m_position + m_orientation * m_vitesse;
        m_pointCible = m_position + m_orientation;
    }


    // Recul de la caméra

    if(input.getTouche(SDL_SCANCODE_DOWN))
    {
        m_position = m_position - m_orientation * m_vitesse;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la gauche

    if(input.getTouche(SDL_SCANCODE_LEFT))
    {
        m_position = m_position + m_deplacementLateral * m_vitesse;
        m_pointCible = m_position + m_orientation;
    }


    // Déplacement vers la droite

    if(input.getTouche(SDL_SCANCODE_RIGHT))
    {
        m_position = m_position - m_deplacementLateral * m_vitesse;
        m_pointCible = m_position + m_orientation;
    }
}

Pensez à renseigner ces deux paramètres lorsque vous initialisez la caméra dans la classe SceneOpenGL :

// Caméra mobile

Camera camera(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0), 0.5, 0.5);

Petit conseil pour terminer, je vous conseille vivement de faire des getters et setters sur ces deux attributs. Ca pourrait être utile dans vos futurs développements :

// Getters et Setters

float getSensibilite() const;
float getVitesse() const;

void setSensibilite(float sensibilite);
void setVitesse(float vitesse);
float Camera::getSensibilite() const
{
    return m_vitesse;
}


float Camera::getVitesse() const
{
    return m_vitesse;
}


void Camera::setSensibilite(float sensibilite)
{
    m_sensibilite = sensibilite;
}


void Camera::setVitesse(float vitesse)
{
    m_vitesse = vitesse;
}

Télécharger : Code Source C++ du chapitre 11

Nous sommes enfin arrivés à la fin de ce chapitre, j'espère que vous êtes toujours en vie. :p

Nous avons vu pas mal de notions mathématiques mais elles sont indispensables dans le développement 3D. Si ça peut vous rassurer, nous n'en reverrons pas beaucoup avant longtemps. Je ne vous ai pas encore parlé des quaternions ça sera un grand moment quand nous aborderons ce chapitre-la. ^^

Enfin, nous avons maintenant une caméra totalement opérationnelle et nous pouvons nous déplacer allègrement dans notre monde 3D. Que diriez-vous maintenant si nous faisions un gros TP récapitulatif ?

Exemple de certificat de réussite
Exemple de certificat de réussite