• 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 08/01/2013

Contrôle avancé de la caméra (Partie 1/2)

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

Ce chapitre en deux parties vient présenter comment créer des caméras contrôlables dans vos applications OpenGL. Fini donc le calvaire de prévoir précisément dans le code la position/orientation de la caméra.
Nous commencerons dans ce chapitre par une caméra Trackball, assez simple à implémenter et qui nous permettra d'introduire le concept de classes en C++.

Principe d'une caméra TrackBall

Le nom TrackBall vient de ce périphérique bizarre qui remplace la souris où l'on tourne directement une boule. Ici j'ai librement tiré le terme du logiciel multiplate-forme Google Earth :

Image utilisateur

Google Earth

Dans Google Earth en effet on utilise la souris pour tourner autour de la Terre. Nous allons donc reproduire ce principe qui nous permettra d'avoir une caméra permettant de regarder un objet / une partie d'une scène sous tous les angles.

Rotation à la souris

En maintenant le bouton gauche de la souris enfoncé, les mouvements de la souris feront tourner la scène :

  • un mouvement horizontal de la souris donne une rotation horizontale de la scène (donc autour de sa verticale).

  • un mouvement vertical de la souris donne une rotation verticale de la scène.

Ces mouvements sont illustrés par les schémas ci-dessous :

Image utilisateur

Mouvement horizontal de la souris

Image utilisateur

Mouvement vertical de la souris

Zoom à la molette

Pour prendre du recul ou au contraire nous rapprocher de l'objet / scène que nous souhaitons visualiser, nous allons tout simplement utiliser la molette. Un coup de molette en avant pour zoomer, un coup de molette en arrière pour dézoomer, rien de plus intuitif :

Image utilisateur

Rotation de la roulette de la souris

Et le clavier ?

Il est possible d'arguer que toutes les souris ne possèdent pas de molette. Dans ce cas-là rien ne vous empêche d'utiliser le clavier pour dézoomer.
Ici nous n'utiliserons le clavier que pour une chose : réintialiser la rotation de la scène avec la touche

Image utilisateur

(SDLK_HOME avec SDL).

Quelques bases de C++

Maintenant que nous savons ce que nous voulons faire avec notre caméra, il faut faire un petit intermède apprentissage du C++.
Nous allons en effet utiliser et regrouper toutes les fonctionnalités de notre caméra dans une classe : TrackBallCamera.

Le cours de M@teo expliquera le concept des classes en détail. Voyons pour l'instant ça comme une extension d'une structure.

Rappelez-vous en C un struct permettait de stocker plusieurs champs dans un même type :

struct NomDeVotreStructure
{
    long variable1;
    long variable2;
    int autreVariable;
    double nombreDecimal;
};

Une classe possède, en plus des attributs, des méthodes. Ces méthodes sont comme des fonctions qui s'appliquent aux instances de cette classe.

Ouh là là beaucoup de mots nouveaux ! Instances par exemple c'est quoi ?

Imaginons une classe nommée Chaise. Une chaise a certains attributs : hauteur, nombre de pieds, matière.
On écrira donc :

class Chaise
{
protected:
        int hauteur;
        int nombre_de_pieds;
        string matiere;
}

Une fois la classe déclarée, dans le c%u0153ur du programme on veut pouvoir en utiliser (des chaises). On crée donc des « instances » de la classe « Chaise » en déclarant simplement une variable de type Chaise.
Exemple :

Chaise machaise;

Vous avez pu voir un mot bizarre dans mon exemple : protected. Sans rentrer dans le détail, cela veut dire que les attributs déclarés protected ne sont pas accessibles de l'extérieur de la classe mais uniquement par ses méthodes.

Les méthodes justement c'est quoi ?

Une méthode est comme une fonction mais elle s'applique à une instance précise de la classe.
Reprenons notre exemple de la chaise. Imaginons que nous voulions enlever un pied à notre chaise.
En C nous aurions dû utiliser une fonction enleverPied en passant en paramètre quelle chaise modifier.

En C++ on appelle directement une méthode sur une instance de la classe.
Exemple :

La déclaration de la classe Chaise dans Chaise.h

class Chaise
{
public:
        void enleverPied();
protected:
        int hauteur;
        int nombre_de_pieds;
        string matiere;
};

L'implémentation des méthodes de la classe Chaise dans Chaise.cpp

#include "chaise.h"

void Chaise::enleverPied()
{
        nombre_de_pieds--;
}

Appel dans le corps du programme
Et maintenant ce qui nous intéresse, l'appel de la méthode enleverPied sur une instance :

Chaise machaise;
machaise.enleverPied();

Comme vous le voyez on appelle une méthode comme on utiliserait un attribut : instance.laméthode();
Quand le programme entre dans le code de la méthode il l'applique donc à l'instance souhaitée, et utilise donc les attributs propres à l'instance en question.

Deux méthodes particulières

Il existe deux méthodes particulières qui ne sont pas appelées directement par l'utilisateur : le constructeur et le destructeur.

Le constructeur est appelé lorsque l'objet est initialisé, généralement à sa déclaration.
C'est une méthode sans type de retour, qui porte le nom de la classe, et qui permet d'initialiser les attributs à des valeurs initiales :

Exemple :

Déclaration du constructeur dans Chaise.h

Class Chaise
{
public:
        Chaise(); //un constructeur ne renvoit rien mais peut éventuellement avoir des paramètres
        void enleverPied();
protected:
        int hauteur;
        int nombre_de_pieds;
        string matiere;
};

Implémentation du constructeur dans Chaise.cpp

Chaise::Chaise()
{
        hauteur = 1;
        nombre_de_pieds = 4;
        matiere = "bois";
}

Et donc ce constructeur sera appelé dès qu'on instanciera un objet dans le corps principal du programme :

Chaise machaise; //déclenche l'appel du constructeur
machaise.enleverPied(); //je sais donc que maintenant elle en a 3 car une chaise a 4 pieds au départ grâce au contructeur

Le destructeur quant à lui est appelé automatiquement quand on détruit l'objet. Dans le cas présent je n'ai rien de spécial à faire dans le destructeur, mais si nous avions alloué de la mémoire dynamiquement (attributs dynamiques de la classe), c'est dans le destructeur qu'il faut les détruire pour ne pas faire de fuite de mémoire. Comme le constructeur, le destructeur porte le nom de la classe précédé du symbole « ~ ». Ici je vais me contenter d'afficher un message lors de la destruction :

Déclaration du destructeur dans Chaise.h

Class Chaise
{
public:
        Chaise(); //un constructeur ne renvoie rien mais peut éventuellement avoir des arguments
        void enleverPied();
        ~Chaise(); //un destructeur ne renvoie rien, n'a pas d'arguments, et se précède du symbole ~
protected:
        int hauteur;
        int nombre_de_pieds;
        string matiere;
};

Implémentation du destructeur dans Chaise.cpp

#include <iostream>
...
Chaise::~Chaise()
{
        std::cout << "Au revoir petite chaise." << std::endl;
}

Dans le corps du programme l'appel au destructeur est automatique à la fin du bloc où l'instance est déclarée.
Exemple :

int main()
{
        Chaise machaise; //appel du constructeur
        machaise.enleverPied(); //la pauvre ça doit faire mal

        return 0; //on quitte le bloc du main, donc on détruit toutes les variables -> appel automatique du destructeur de Chaise sur l'instance machaise.
}

Allocation dynamique

Si vous en êtes à la lecture du tuto OpenGL c'est que vous connaissez sûrement l'allocation dynamique en C :

struct Chaise * machaise;
machaise = malloc(sizeof(struct Chaise));

En C++ on utilise généralement l'opérateur new comme ceci :

Chaise * machaise;
machaise = new Chaise();

On note ici l'utilisation des ( ) après Chaise qui montre clairement qu'on cherche à construire un objet. Une fois la mémoire allouée, le constructeur de la classe est donc automatiquement appelé.

Pour la destruction, delete remplace le free que vous connaissez :

Chaise * machaise;
machaise = new Chaise();
machaise->enleverPied();
delete machaise;

Représentation UML

Je ne vais pas vous faire un cours de modélisation UML mais juste vous présenter une manière graphique de représenter une classe. J'utiliserai ce symbolisme tout au long du tuto pour résumer brièvement les fonctionnalités d'une classe :

Image utilisateur

Ce qui donne par exemple pour reprendre notre chère chaise :

Image utilisateur

Implémentation de la caméra

Nous allons implémenter la caméra TrackBall avec le concept de classe que nous venons de voir.
Nous l'avons vu plus haut il nous faut gérer trois types d'événements :

  • l'appui sur le bouton gauche de la souris : nous n'activerons le mouvement à la souris que si ce bouton est enfoncé ;

  • le mouvement de la souris : pour changer l'orientation de la scène ;

  • l'appui sur la touche HOME pour remettre l'orientation de la scène à sa valeur initiale.

Dans le corps principal de notre programme SDL (partie suivante) nous devrons donc envoyer les événements nécessaires au fonctionnement de la caméra.

Vous savez comment placer une caméra manuellement avec gluLookAt. Ici c'est la méthode look de notre classe TrackBallCamera qui s'occupera d'appeler le gluLookAt pour nous. Dans le code d'affichage de la scène nous n'aurons donc qu'à appeler cette méthode.

Nous allons aussi rajouter deux autres méthodes pour configurer la sensibilité de notre caméra :

  • setMotionSensivity : pour déterminer la vitesse de rotation de la scène en fonction du mouvement en pixel du curseur de la souris ;

  • setScrollSensivity : pour déterminer de combien zoomer/dézoomer lorsque l'on utilise la molette de la souris.

Tout cela se traduit donc de la façon suivante en UML et C++ :

UML simplifié

Déclaration C++

Image utilisateurImage utilisateur
class TrackBallCamera
{
public:
    TrackBallCamera();

    virtual void OnMouseMotion(const SDL_MouseMotionEvent & event);
    virtual void OnMouseButton(const SDL_MouseButtonEvent & event);
    virtual void OnKeyboard(const SDL_KeyboardEvent & event);

    virtual void look();
    virtual void setMotionSensivity(double sensivity);
    virtual void setScrollSensivity(double sensivity);

    virtual ~TrackBallCamera();
protected:
    double _motionSensivity;
    double _scrollSensivity;
    bool _hold;
    double _distance;
    double _angleY;
    double _angleZ;
    SDL_Cursor * _hand1;
    SDL_Cursor * _hand2;
};;

J'en ai profité pour rajouter tous les attributs que nous allons utiliser. Une petite explication s'impose donc :

  • double _motionSensivity : utilisé pour stocker la sensibilité de la caméra aux mouvements de la souris ;

  • double _scrollSensivity : sensibilité de la caméra au scroll de la souris (« pas » d'un déplacement) ;

  • bool _hold : est-on actuellement en train de maintenir le bouton gauche de la souris enfoncé ?

  • double _distance : distance entre la caméra et le centre de la scène ;

  • double _angleY : angle de rotation verticale de la scène (en vert sur le schéma plus haut) ;

  • double _angleZ : angle de rotation horizontale de la scène (donc autour de la verticale, en bleu sur le schéma).

Les deux derniers attributs sont les deux curseurs de la souris que nous utiliserons :

Image utilisateur

_hand1 en temps normal, _hand2 quand le bouton gauche de la souris est enfoncé.

Constructeur

Dans le constructeur nous allons simplement initialiser tous les attributs à des valeurs initiales connues. Il ne faut rien laisser qui puisse être utilisé sans avoir été initialisé.

La partie la moins évidente est peut-être la création des deux curseurs. Pour faciliter les choses j'ai relégué tout le travail dans une fonction rajoutée à sdlglutils : cursorFromXPM (fournie dans l'archive finale).

TrackBallCamera::TrackBallCamera()
{
    const char *hand1[] =
        {
            /* width height num_colors chars_per_pixel */
            " 16 16 3 1 ",
            /* colors */
            "X c #000000",
            ". c #ffffff",
            "  c None",
            /* pixels */
            "       XX       ",
            "   XX X..XXX    ",
            "  X..XX..X..X   ",
            "  X..XX..X..X X ",
            "   X..X..X..XX.X",
            "   X..X..X..X..X",
            " XX X.......X..X",
            "X..XX..........X",
            "X...X.........X ",
            " X............X ",
            "  X...........X ",
            "  X..........X  ",
            "   X.........X  ",
            "    X.......X   ",
            "     X......X   ",
            "     X......X   ",
            "0,0"
        };

    const char *hand2[] =
        {
            /* width height num_colors chars_per_pixel */
            " 16 16 3 1 ",
            /* colors */
            "X c #000000",
            ". c #ffffff",
            "  c None",
            /* pixels */
            "                ",
            "                ",
            "                ",
            "                ",
            "    XX XX XX    ",
            "   X..X..X..XX  ",
            "   X........X.X ",
            "    X.........X ",
            "   XX.........X ",
            "  X...........X ",
            "  X...........X ",
            "  X..........X  ",
            "   X.........X  ",
            "    X.......X   ",
            "     X......X   ",
            "     X......X   ",
            "0,0"
        };
    _hand1 = cursorFromXPM(hand1); //création du curseur normal
    _hand2 = cursorFromXPM(hand2); //création du curseur utilisé quand le bouton est enfoncé
    SDL_SetCursor(_hand1); //activation du curseur normal
    _hold = false; //au départ on part du principe que le bouton n'est pas maintenu
    _angleY = 0;
    _angleZ = 0;
    _distance = 2; //distance initiale de la caméra avec le centre de la scène
    _motionSensivity = 0.3;
    _scrollSensivity = 1;
}

On Mouse Motion

Cette méthode est la plus importante de la classe et pourtant l'une des plus courtes. Rappelez-vous le principe de la caméra : lorsque le curseur de la souris est bougé, si le bouton gauche de la souris est maintenu appuyé, alors la scène tourne. Voyons donc comment cela se traduit en code :

void TrackBallCamera::OnMouseMotion(const SDL_MouseMotionEvent & event)
{
    if (_hold) //si nous maintenons le bouton gauche enfoncé
    {
        _angleZ += event.xrel*_motionSensivity; //mouvement sur X de la souris -> changement de la rotation horizontale
        _angleY += event.yrel*_motionSensivity; //mouvement sur Y de la souris -> changement de la rotation verticale
        //pour éviter certains problèmes, on limite la rotation verticale à des angles entre -90° et 90°
        if (_angleY > 90)
            _angleY = 90;
        else if (_angleY < -90)
            _angleY = -90;
    }
}

On Mouse Button

Cette méthode nous permet de gérer deux choses :

  • l'appui et le relâchement du bouton gauche de la souris ;

  • le mouvement de la molette de la souris.

void TrackBallCamera::OnMouseButton(const SDL_MouseButtonEvent & event)
{
    if (event.button == SDL_BUTTON_LEFT) //l'événement concerne le bouton gauche
    {
        if ((_hold)&&(event.type == SDL_MOUSEBUTTONUP)) //relâchement alors qu'on était enfoncé
        {
            _hold = false; //le mouvement de la souris ne fera plus bouger la scène
            SDL_SetCursor(_hand1); //on met le curseur normal
        }
        else if ((!_hold)&&(event.type == SDL_MOUSEBUTTONDOWN)) //appui alors qu'on était relâché
        {
            _hold = true; //le mouvement de la souris fera bouger la scène
            SDL_SetCursor(_hand2); //on met le curseur spécial
        }
    }
    else if ((event.button == SDL_BUTTON_WHEELUP)&&(event.type == SDL_MOUSEBUTTONDOWN)) //coup de molette vers le haut
    {
        _distance -= _scrollSensivity; //on zoome, donc rapproche la caméra du centre
        if (_distance < 0.1) //distance minimale, à changer si besoin (avec un attribut par exemple)
            _distance = 0.1;
    }
    else if ((event.button == SDL_BUTTON_WHEELDOWN)&&(event.type == SDL_MOUSEBUTTONDOWN)) //coup de molette vers le bas
    {
            _distance += _scrollSensivity; //on dézoome donc éloigne la caméra
    }
}

OnKeyboard

La dernière méthode qui vient utiliser les événements est la gestion du clavier, pour l'appui sur la touche HOME. On se contente d'y remettre la rotation de la scène à zéro :

void TrackBallCamera::OnKeyboard(const SDL_KeyboardEvent & event)
{
    if ((event.type == SDL_KEYDOWN)&&(event.keysym.sym == SDLK_HOME)) //appui sur la touche HOME
    {
        _angleY = 0; //remise à zéro des angles
        _angleZ = 0;
    }
}

Look

Tout cela est bien beau, nous savons comment changer des variables avec la souris et le clavier mais ça ne fait en rien bouger la caméra dans notre scène. En effet nous n'avons pour l'instant pas vu la moindre commande OpenGL !
Il est donc temps de s'y mettre avec la méthode Look qui viendra remplacer, dans votre fonction d'affichage, l'appel à gluLookAt.

Remplacer gluLookAt ? Parce qu'il y a un truc mieux que tu nous as caché !! ?

Non non. La méthode Look appelle elle-même gluLookAt mais avec des paramètres qui dépendent de la position de la caméra, c'est pour ça que vous n'avez plus à l'appeler vous-mêmes.
Si on se réfère aux schémas en début de chapitre qui expliquent le principe de la caméra TrackBall, on remarque plusieurs choses :

  • elle regarde le centre de la scène ;

  • ce n'est pas la caméra mais la scène qui est tournée autour de Y et Z.

Il suffit alors de traduire tout ça en code :

void TrackBallCamera::look()
{
    gluLookAt(_distance,0,0,
              0,0,0,
              0,0,1); // la caméra regarde le centre (0,0,0) et est sur l'axe X à une certaine distance du centre donc (_distance,0,0)
    glRotated(_angleY,0,1,0); //la scène est tournée autour de l'axe Y
    glRotated(_angleZ,0,0,1); //la scène est tournée autour de l'axe Z
}

Et voilà ce n'était vraiment pas sorcier.
La dernière chose qu'il nous reste à faire dans le code même de la caméra est une destruction propre de ce qui a été alloué dynamiquement.

Destructeur

Les seules choses allouées dynamiquement sont les curseurs qu'il nous faut détruire quand la caméra est détruite :

TrackBallCamera::~TrackBallCamera()
{
    SDL_FreeCursor(_hand1); //destruction du curseur normal
    SDL_FreeCursor(_hand2); //destruction du curseur spécial
    SDL_SetCursor(NULL); //on remet le curseur par défaut.
}

Scène de test

Le code de la classe TrackBallCamera est complet et n'a besoin de rien de plus. Cependant un objet camera ne va pas recevoir tout seul les événements, il faut les lui donner. Nous allons donc voir avec une petite scène de test simple comment utiliser la caméra que nous venons de créer. Pour faire original et pas du tout inspiré de Google Earth, nous allons créer une sphère avec la texture de de la Terre. Hum hum ! :-°

En variables globales nous allons donc utiliser :

GLuint earth; //l'identifiant de la texture de la Terre
TrackBallCamera * camera; //un pointeur vers notre caméra

Pourquoi pas directement une caméra ?

Rappelez-vous, le constructeur appelle des fonctions SDL qui nécessitent qu'une fenêtre SDL existe déjà. Il ne faut donc pas que la caméra soit construite dès le lancement du programme (ce qui serait le cas ici si nous n'utilisions pas de pointeur). Nous la créons donc dynamiquement après la fenêtre :

atexit(stop); //stop() sera appelé quand on fera exit(0);
//...
        SDL_SetVideoMode(width, height, 32, SDL_OPENGL);
//...
        earth = loadTexture("EarthMap.jpg");
        camera = new TrackBallCamera();
        camera->setScrollSensivity(0.1);

Comme la caméra a été créée dynamiquement c'est à nous de la détruire proprement à la fin de l'exécution du programme. C'est ce qu'on fait dans la fonction stop, appelée quand on fera exit(0); dans le corps du programme :

void stop()
{
    delete camera; //destruction de la caméra allouée dynamiquement
    SDL_Quit();
}

Comme je vous l'ai dit plus haut, la caméra ne recevra pas les événements clavier/souris si on ne les lui donne pas. C'est pourquoi dans notre partie de gestion des événements, il faut donner à la caméra les événements dont on ne se sert pas :

while(SDL_PollEvent(&event))
        {
            switch(event.type)
            {
                case SDL_QUIT:
                exit(0);
                break;
                case SDL_KEYDOWN:
                switch (event.key.keysym.sym)
                {
                    case SDLK_p:
                    takeScreenshot("test.bmp");
                    break;
                    case SDLK_ESCAPE:
                    exit(0);
                    break;
                    default : //on a utilisé la touche P et la touche ECHAP, le reste est donné à la caméra
                    camera->OnKeyboard(event.key);
                }
                break;
                case SDL_MOUSEMOTION: //la souris est bougée, ça n'intéresse que la caméra
                camera->OnMouseMotion(event.motion);
                break;
                case SDL_MOUSEBUTTONUP:
                case SDL_MOUSEBUTTONDOWN:
                camera->OnMouseButton(event.button); //tous les événements boutons (up ou down) sont donnés à la caméra
                break;
            }
        }

La dernière chose qu'il nous reste à faire est de dessiner la scène, ici très basique.

void DrawGL()
{
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    camera->look();

    GLUquadric* params = gluNewQuadric();
    gluQuadricTexture(params,GL_TRUE);
    glBindTexture(GL_TEXTURE_2D,earth);
    gluSphere(params,1,20,20);
    gluDeleteQuadric(params);

    glFlush();
    SDL_GL_SwapBuffers();
}
Image utilisateur

Téléchargez la vidéo au format avi/Xvid (1.17 Mo)

Téléchargez le projet Code::Blocks, l'exécutable Windows et le Makefile Unix (1.30 Mo)

Et voilà finie la prise de tête de prévoir à l'avance comment placer la caméra pour bien voir votre scène !

Dans la chapitre suivant nous verrons une caméra encore plus intéressante mais un peu plus dure à implémenter : la caméra FreeFly qui nous permettra de voler librement dans notre scène.

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