Mis à jour le mardi 8 janvier 2013
  • 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 !

Enfin de la 3D (Partie 2/2)

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

Dans le dernier chapitre nous nous sommes préparés à passer à la 3D, il est donc temps de s'y mettre et d'entamer notre premier dessin : un cube ! En dessinant notre cube et en l'animant, nous rencontrerons des problèmes, prévus d'avance rassurez-vous ^^ , et nous verrons comment les résoudre.

Un cube

L'exemple du cube est assez simple et nous continuerons avec lors de la création de notre première scène texturée.

Voyons tout d'abord comment est constitué un cube :

Image utilisateur

Coordonnées des sommets du cube

Un cube est composé de 8 sommets et 6 faces, chaque face faisant intervenir 4 sommets.
Nous n'allons pas nous contenter de dessiner chacun des 8 sommets, nous n'aurions pas de faces pleines. Il nous faut donc décrire les faces une par une, en indiquant les sommets qu'elles font intervenir.

Décrire des sommets en 3D

Ici nous devons définir 3 coordonnées pour chaque sommet : X, Y et Z. Nous ne pouvons donc plus utiliser le basique glVertex2d que nous utilisions auparavant. Il va falloir donc utiliser la version avec 3 arguments soit glVertex3d.

Exemple :

glVertex3d(1,1,1);

Décrire le cube

Les faces étant des carrés, nous allons utiliser le mode GL_QUADS pour décrire les vertices.
Pour différencier les faces nous leur attribuerons une couleur différente ; nous ferons la première (celle avec les flèches) en rouge. En ce qui concerne la caméra, j'ai choisi de la placer en (3,4,2) pour regarder le cube centré en (0,0,0), car cela donnera un bon angle de vue (c'est la position utilisée pour le schéma plus haut).

Ce qui donne donc :

void Dessiner()
{
    glClear( GL_COLOR_BUFFER_BIT );

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    gluLookAt(3,4,2,0,0,0,0,0,1);

    glBegin(GL_QUADS);

    glColor3ub(255,0,0); //face rouge
    glVertex3d(1,1,1);
    glVertex3d(1,1,-1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,1,1);
    glEnd();

    glFlush();
    SDL_GL_SwapBuffers();
}

Facile ! Il suffit de choisir un point de départ et de suivre le contour de la face pour décrire les sommets un par un.

Continuons donc avec la 2e face, disons celle à gauche de la première (quand on regarde le schéma) et faisons-la en vert.

glColor3ub(0,255,0); //face verte
    glVertex3d(1,-1,1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,1,-1);
    glVertex3d(1,1,1);

Ce qui me donne le résultat suivant :

Image utilisateur

Bon maintenant parce que je veux vous montrer un problème important, attaquons-nous à la face de derrière que nous ferons... devinez... en bleu ! ;)

Rien de compliqué, il suffit de suivre le schéma de tout à l'heure pour avoir rapidement les coordonnées et écrire le code approprié.

glColor3ub(0,0,255); //face bleue
    glVertex3d(-1,-1,1);
    glVertex3d(-1,-1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,-1,1);

Et voilà le résultat :

Image utilisateur

o_O o_O o_O

En effet vous ne rêvez pas, la face bleue qui était censée être derrière, donc en majeure partie cachée par la rouge et la verte vient se dessiner par-dessus ces dernières.
Et c'est tout à fait logique, OpenGL dessine les carrés dans l'ordre dans lequel on les définit. Il ne se soucie pour l'instant pas de savoir s'il y a déjà quelque chose là où il dessine et vient donc écraser les faces précédentes.

La solution ? Le Z-Buffer !

Le Z-Buffer

Le Z-Buffer ou Depth-Buffer (pour tampon de profondeur) sert à éviter le problème que nous venons de rencontrer.

Principe du Z-Buffer

Le Z-Buffer est un tampon (buffer) qui stocke la profondeur (d'où le Z, X et Y sur l'écran étant la position en pixel) de chaque pixel affiché à l'écran.
Ensuite quand OpenGL demande à dessiner un pixel à un endroit, il compare la profondeur du point à afficher et celle présente dans le buffer. Si le nouveau pixel est situé devant l'ancien, alors il est dessiné et la valeur de la profondeur dans le buffer est mise à jour. Sinon, le pixel était alors situé derrière et n'a donc pas lieu d'être affiché.

Pour bien comprendre, suivons le cheminement qui est fait.

  • Image utilisateur

    Au départ le buffer (ici de taille 3x3 pour l'exemple) est initialisé à des distances infinies (le plus loin possible vers le fond de votre écran).

  • Image utilisateur

    OpenGL demande à dessiner un pixel en (2,2,5).
    Valeur demandée : 5.
    Valeur présente : infini.
    5 < infini => OK pour dessin
    Dessin du pixel + Mise à jour du Z-buffer avec la valeur 5.

  • Demande de dessin d'un pixel en (2,2,10).
    Valeur demandée : 10.
    Valeur présente : 5.
    10 > 5 => Dessin refusé

Application dans OpenGL

Heureusement pour nous OpenGL gère très bien cette technique, il nous faut juste modifier notre programme pour l'activer et bien l'utiliser !

Pour cela il nous faut :

  • activer son utilisation : après la création de la fenêtre OpenGL il faut simplement appeler :

    glEnable(GL_DEPTH_TEST);
  • le réinitialiser à chaque nouvelle image, en même temps que le buffer des pixels :

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;

Ce qui nous donne un code complet (avec le début de notre cube) :

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib>

void Dessiner();

int main(int argc, char *argv[])
{
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);
    atexit(SDL_Quit);
    SDL_WM_SetCaption("SDL GL Application", NULL);
    SDL_SetVideoMode(640, 480, 32, SDL_OPENGL);

    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluPerspective(70,(double)640/480,1,1000);
    glEnable(GL_DEPTH_TEST);

    Dessiner();

    for (;;)
    {
        SDL_WaitEvent(&event);

        switch(event.type)
        {
            case SDL_QUIT:
            exit(0);
            break;
        }
        Dessiner();

    }

    return 0;
}

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

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    gluLookAt(3,4,2,0,0,0,0,0,1);

    glBegin(GL_QUADS);

    glColor3ub(255,0,0); //face rouge
    glVertex3d(1,1,1);
    glVertex3d(1,1,-1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,1,1);

    glColor3ub(0,255,0); //face verte
    glVertex3d(1,-1,1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,1,-1);
    glVertex3d(1,1,1);

    glColor3ub(0,0,255); //face bleue
    glVertex3d(-1,-1,1);
    glVertex3d(-1,-1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,-1,1);

    glEnd();

    glFlush();
    SDL_GL_SwapBuffers();
}

Et en effet en exécutant notre nouveau code nous obtenons ceci :

Image utilisateur

Ouf ! Nous avons enfin ce que nous désirions. Bien pratique ce z-buffer !

Finir le cube

À ce stade il ne vous reste plus qu'à compléter le code avec les 3 faces restantes et leur choisir de belles couleurs.
N'oubliez pas que vous pouvez utiliser le schéma du début pour facilement trouver les coordonnées des sommets de la face en cours.

Voici mon code pour ceux qui ne veulent pas essayer eux-mêmes, ou simplement pour comparer :

glBegin(GL_QUADS);

    glColor3ub(255,0,0); //face rouge
    glVertex3d(1,1,1);
    glVertex3d(1,1,-1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,1,1);

    glColor3ub(0,255,0); //face verte
    glVertex3d(1,-1,1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,1,-1);
    glVertex3d(1,1,1);

    glColor3ub(0,0,255); //face bleue
    glVertex3d(-1,-1,1);
    glVertex3d(-1,-1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,-1,1);

    glColor3ub(255,255,0); //face jaune
    glVertex3d(-1,1,1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,-1,-1);
    glVertex3d(-1,-1,1);

    glColor3ub(0,255,255); //face cyan
    glVertex3d(1,1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(-1,-1,-1);
    glVertex3d(-1,1,-1);

    glColor3ub(255,0,255); //face magenta
    glVertex3d(1,-1,1);
    glVertex3d(1,1,1);
    glVertex3d(-1,1,1);
    glVertex3d(-1,-1,1);

    glEnd();

Et le résultat graphique correspondant :

Image utilisateur
Image utilisateur

Comme vous avez le code sous les yeux vous savez que vous n'avez pas triché et que le code fait bien un cube 3D. Mais personnellement je vois 3 quadrilatères de couleur, je peux faire pareil sous Paint en 30 secondes, la preuve :
Ok c'est moche et mal fait mais avec un peu de soin j'aurais pu avoir pareil ! :honte:

Il est donc temps de profiter de la puissance de la 3D temps réel et d'animer notre cube pour le voir sous toutes les coutures !

Animation

Pour animer notre cube nous allons simplement le faire tourner en utilisant ce que vous connaissez déjà par coeur : la rotation !

Au niveau du dessin nous n'avons vraiment pas grand chose à changer, juste à faire tourner le repère avant de dessiner le cube. Nous allons le faire tourner à la fois sur Z (la verticale) et X donc nous avons besoin de 2 variables globales* pour chacun des angles à contrôler.

Image utilisateur

* En général en programmation on essaye d'éviter les variables globales mais ici nous faisons en quelque sorte du prototypage pour apprendre et tester les concepts OpenGL, ce n'est donc vraiment pas bien grave.

Le code, simplifié, du programme devient alors :

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib>

void Dessiner();

double angleZ = 0;
double angleX = 0;

int main(int argc, char *argv[])
{
   //le code du main
}

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

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    gluLookAt(3,4,2,0,0,0,0,0,1);

    glRotated(angleZ,0,0,1);
    glRotated(angleX,1,0,0);

    //dessin du cube

    glFlush();
    SDL_GL_SwapBuffers();
}

En terme de code SDL nous voulons que ces angles soient modifiés automatiquement avec le temps. On ne peut donc plus se permettre d'attendre les événements avec SDL_WaitEvent et nous allons en conséquence utiliser SDL_PollEvent pour récupérer les événements s'il y en a puis animer notre cube.

Nous modifions donc le code de notre boucle d'affichage pour incrémenter nos angles à chaque image :

for (;;)
    {
        while (SDL_PollEvent(&event))
        {

            switch(event.type)
            {
                case SDL_QUIT:
                exit(0);
                break;
            }
        }

        angleZ += 1;
        angleX += 1;

        Dessiner();

    }

Gérer la vitesse d'animation

En testant le code actuel vous voyez que le cube tourne beaucoup trop vite, nous n'avons franchement rien le temps de voir.
Il faut donc introduire des vitesses de rotation. Ces vitesses ne doivent pas dépendre de l'ordinateur sur lequel le programme est lancé et donc doivent prendre en compte le temps réel.
Pour ce faire, à chaque image (chaque passage dans la boucle donne lieu à une image), il faut déterminer combien de temps il s'est passé depuis la dernière image et faire bouger le cube en conséquence.

Nous avons donc besoin de 3 variables :

  • une pour garder en mémoire le temps qu'il était lors de la dernière image : last_time ;

  • une pour avoir le temps de l'image actuelle : current_time ;

  • une (par commodité) pour le temps écoulé : ellapsed_time.

Le principe est alors le suivant.

  1. On initialise une première fois, avant de rentrer dans notre boucle d'affichage last_time avec le temps actuel.

  2. À chaque image on récupère le temps actuel dans current_time.

  3. On utilise la différence entre le temps actuel et le temps qu'il était lors de l'ancien passage pour savoir combien de temps s'est écoulé. On stocke le résultat dans ellapsed_time.

  4. On réalise nos mouvements en fonction du temps écoulé.

  5. On finit par affecter à last_time la valeur de current_time car nous passons à une nouvelle image et donc le présent devient du passé ( :'( c'est beau !)

  6. .

En ce qui concerne l'unité de mesure des vitesses, comme le temps écoulé est donné en millisecondes et que les angles utilisés dans glRotate sont en degrés, il s'agit tout simplement de degrés par milliseconde.

Dans notre cas j'utiliserai 0.05 °/ms.

La traduction en code du principe tout juste évoqué est la suivante :

Uint32 last_time = SDL_GetTicks();
    Uint32 current_time,ellapsed_time;

    for (;;)
    {
        while (SDL_PollEvent(&event))
        {

            switch(event.type)
            {
                case SDL_QUIT:
                exit(0);
                break;
            }
        }

        current_time = SDL_GetTicks();
        ellapsed_time = current_time - last_time;
        last_time = current_time;

        angleZ += 0.05 * ellapsed_time;
        angleX += 0.05 * ellapsed_time;

        Dessiner();

    }
Image utilisateur

Ne pas monopoliser le CPU

Ça rame ! Le programme prend 100% du CPU rien que pour faire tourner un simple cube, je ne vais jamais pouvoir faire un jeu !

En effet si on regarde la charge du processeur imposée par notre application on voit qu'il est totalement occupé à gérer notre programme :

Image utilisateur

Cela ne veut pas dire que votre programme est lent c'est juste que nous bouclons en permanence et que nous ne prenons jamais de pause. En réalité nous n'avons pas vraiment besoin de boucler tout le temps. Nous allons utiliser une technique possible (celle que je préfère et donc souhaite vous expliquer) : limiter les FPS (frames per second - images par seconde).
En effet pour avoir une animation très fluide il nous suffit de 50 images par seconde.

En fixant le nombre d'images par secondes désirées par votre application, il est alors possible de la faire s'endormir un certain temps si elle va plus vite que nécessaire, ce qui soulagera (même s'il ne s'en plaint pas) le processeur.
Pour ce faire nous allons calculer à chaque image combien de temps nous avons mis pour la dessiner, si nous avons été plus rapides que le temps moyen nécessaire, nous stopperons l'exécution pour un certain temps.

Par exemple, autoriser 50 images par seconde donne à chaque image 20 millisecondes pour s'afficher. Imaginons qu'une image mette 5 ms à s'afficher réellement, il reste alors 15 ms à tuer. Plutôt que de passer directement à l'image suivante, nous allons endormir l'application pendant ces 15 ms.

En terme de code nous allons utiliser SDL_GetTicks comme auparavant pour déterminer le temps écoulé entre le début et la fin de la création de l'image. Nous utiliserons SDL_Delay pour suspendre temporairement l'application.

Uint32 start_time; //nouvelle variable

    for (;;)
    {
        start_time = SDL_GetTicks(); 
        while (SDL_PollEvent(&event))
        {

            switch(event.type)
            {
                case SDL_QUIT:
                exit(0);
                break;
                case SDL_KEYDOWN:
                animation = !animation;
                break;
            }
        }

        current_time = SDL_GetTicks();
        ellapsed_time = current_time - last_time;
        last_time = current_time;

        angleZ += 0.05 * ellapsed_time;
        angleX += 0.05 * ellapsed_time;

        Dessiner();

        ellapsed_time = SDL_GetTicks() - start_time;
        if (ellapsed_time < 10)
        {
            SDL_Delay(10 - ellapsed_time);
        }

    }

    return 0;
}

Notez qu'ici nous ne voulons pas savoir combien de temps il s'est passé depuis la dernière fois mais combien de temps notre image a pris à se dessiner (j'y ai inclus la gestion des événements). Il y a donc un appel à SDL_GetTicks au début de notre boucle et un appel à la toute fin. Je réutilise ellapsed_time par commodité mais pas les autres variables pour ne pas mélanger les 2 concepts : limitation des FPS et gestion du temps dans les animations.

Code final :

#include <SDL/SDL.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <cstdlib>

void Dessiner();

double angleZ = 0;
double angleX = 0;

int main(int argc, char *argv[])
{
    SDL_Event event;

    SDL_Init(SDL_INIT_VIDEO);
    atexit(SDL_Quit);
    SDL_WM_SetCaption("SDL GL Application", NULL);
    SDL_SetVideoMode(640, 480, 32, SDL_OPENGL);

    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    gluPerspective(70,(double)640/480,1,1000);

    glEnable(GL_DEPTH_TEST);

    Dessiner();

    Uint32 last_time = SDL_GetTicks();
    Uint32 current_time,ellapsed_time;
    Uint32 start_time;

    for (;;)
    {
        start_time = SDL_GetTicks();
        while (SDL_PollEvent(&event))
        {

            switch(event.type)
            {
                case SDL_QUIT:
                exit(0);
                break;
            }
        }

        current_time = SDL_GetTicks();
        ellapsed_time = current_time - last_time;
        last_time = current_time;

        angleZ += 0.05 * ellapsed_time;
        angleX += 0.05 * ellapsed_time;

        Dessiner();

        ellapsed_time = SDL_GetTicks() - start_time;
        if (ellapsed_time < 10)
        {
            SDL_Delay(10 - ellapsed_time);
        }

    }

    return 0;
}

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

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity( );

    gluLookAt(3,4,2,0,0,0,0,0,1);

    glRotated(angleZ,0,0,1);
    glRotated(angleX,1,0,0);

    glBegin(GL_QUADS);

    glColor3ub(255,0,0); //face rouge
    glVertex3d(1,1,1);
    glVertex3d(1,1,-1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,1,1);

    glColor3ub(0,255,0); //face verte
    glVertex3d(1,-1,1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,1,-1);
    glVertex3d(1,1,1);

    glColor3ub(0,0,255); //face bleue
    glVertex3d(-1,-1,1);
    glVertex3d(-1,-1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(1,-1,1);

    glColor3ub(255,255,0); //face jaune
    glVertex3d(-1,1,1);
    glVertex3d(-1,1,-1);
    glVertex3d(-1,-1,-1);
    glVertex3d(-1,-1,1);

    glColor3ub(0,255,255); //face cyan
    glVertex3d(1,1,-1);
    glVertex3d(1,-1,-1);
    glVertex3d(-1,-1,-1);
    glVertex3d(-1,1,-1);

    glColor3ub(255,0,255); //face magenta
    glVertex3d(1,-1,1);
    glVertex3d(1,1,1);
    glVertex3d(-1,1,1);
    glVertex3d(-1,-1,1);

    glEnd();

    glFlush();
    SDL_GL_SwapBuffers();
}

En comparaison, pour une animation de la même fluidité, la charge moyenne du processeur est négligeable :

Image utilisateur

Notes diverses :

  • il est aussi possible d'utiliser des timers pour limiter les FPS (voir la doc de SDL_AddTimer ainsi que son exemple). Ce n'est pas la solution retenue ici ;

  • les adeptes de la doc ne manqueront pas de signaler le problème de la granularité du temps de pause (cf. SDL_Delay). Dans les tests effectués pour rédiger ce tutoriel, le temps de pause réel n'excédait jamais le temps demandé de plus de 1 milliseconde. Le nombre réel d'images par seconde est donc égal (ou très proche) au nombre fixé ;

  • en terme d'ergonomie cela peut faire peur à certains de faire s'endormir le programme un certain temps. Qu'ils soient rassurés, même dans une application 3D bien plus complexe, la gestion des événements n'est en rien altérée.

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

Téléchargez la vidéo au format avi/Xvid (414 Ko)

Voilà le mystère de la 3D en OpenGL est enfin tombé !
Vous savez maintenant créer de toutes pièces un objet 3D et réaliser une animation.
En attendant le prochain chapitre sur les textures vous pouvez, si vous le souhaitez, améliorer votre programme pour créer de multiples objets, en animer certains grâce au clavier, et par exemple faire tourner la caméra autour de votre scène.
Bonne création !

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