Enfin de la 3D

1.  Les primitives graphiques

Le début du dessin d’une primitive se fait avec glBegin : ici un polygone. Les possibilités sont tirées de la man page de glBegin :

           Le format général des fonctions d’OpenGL est : {nom}{1234}{b s i d ub us ui}{v}, respectivement le nom de la fonction, le nombre de paramètres, leur type, passé directement ou par pointeur (vecteur). Tous les caractéristiques de la fonction sont optionnelles sauf le nom.

            Le début du dessin d'une primitive commence par glBegin(). Elle est suivie de la définitition des différents points composant l'objet. Chaque point est défini grâce à la fonction glVertex, qui existe sous plusieurs formes : glVertex2d, glVertex2f, glVertex2i, glVertex2s, glVertex3d, glVertex3f, glVertex3i, glVertex3s, glVertex4d, glVertex4f, glVertex4i, glVertex4s, glVertex2dv, glVertex2fv, glVertex2iv, glVertex2sv, glVertex3dv, glVertex3fv, glVertex3iv
glColor() permet de fixer la couleur courante. De même, elle existe sous de nombreuses formes.
glEnd(), quant à elle, indique la fin de l’entrée de coordonnées.

            Cependant, jusqu’ici, rien n’est affiché : OpenGL a mis tout ce qu’il y a à faire dans un buffer, mais ne l’affiche que lorsqu’on lui demande, avec glFlush(), qui rend la main immédiatement, ou avec glFinish(), qui attend que tout soit affiché (peu d’intêret).

2. Exemple simple : un triangle

Les fonctions suivantes permettent de dessiner un triangle à l’écran :

glBegin(GL_TRIANGLES);                                //commence le dessin d'un triangle
            glColor3f(0.0f,1.0f,0.0f);           
             //couleur du premier sommet
            glVertex3f(-65.0f,65.0f,0.0f);                    //coordonnées du premier sommet
            glColor3f(1.0f,0.0f,0.0f);                         //etc ...
            glVertex3f(65.0f,-65.0f,0.0f);
            glColor3f(0.0f,0.0f,1.0f);
            glVertex3f(-65.0f,-65.0f,0.0f);
glEnd();                                                         //fin du tracé du triangle

Sources et éxécutable

       Pour obtenir le dégradé des couleurs, il a simplement fallu déclarer les 3 sommets avec des couleurs différentes et l’OpenGL l’a fait, tout seul, comme un grand. On peut bien sûr désactiver cette fonctionnalité avec glShadeModel(GL_FLAT), qui demande à l’OpenGL de remplir le polygone avec la couleur du dernier point specifié. Pour la réaciver, il faut taper : glShadeModel(GL_SMOOTH).

3.  Bouger ce triangle : rotations, translations, symétries

            1. Rotation et translation

            Pour bouger la « caméra » ou un objet (c’est la même chose de bouger tout les objets ou la caméra), il faut utiliser la matrice de vue (GL_MODELVIEW). Comme calculer directement une matrice de changement de repère n’est pas vraiment évidente, il existe des fonctions prédéfinies : glRotate et glTranslate.

   void glRotated( GLdouble angle, GLdouble x, GLdouble y, GLdouble z )

   void glRotatef( GLfloat angle, GLfloat x, GLfloat y, GLfloat z )

   void glTranslated( GLdouble x, GLdouble y, GLdouble z )

   void glTranslatef( GLfloat x, GLfloat y, GLfloat z )

Ces fonctions multiplient à droite la matrice de vue. Il faut donc faire attention à l’ordre dans lequel les transformations sont entrées : c’est la dernière transformation entrée qui sera appliquée en premier.

Rappels :
-     soient f de matrice L, g de matrice M et h de matrice N. h°g°f a pour matrice N.M.L donc il faut entrer les matrices dans l'ordre N,M,L
-     les rotations et les translations ne sont pas commutatives. Par exemple, pour faire tourner un objet sur lui-même, il faut entrer en dernier la matrice de rotation. Pour simplifier, l’origine du repère local d’un objet est par convention  son centre de rotation (d’inertie) pour permettre de le faire tourner sur lui-même sans calcul complexe.

Tout cela peut paraître compliqué, mais un ordinateur ne sait comprendre que les matrices, et un énorme avantage est le temps de calcul : même si de nombreuses transformations sont entrées, il ne reste qu’une seule matrice à appliquer à tout les points entrés.

            2.Pile de matrice

            Mais comment inverser les transformations entrées ? Calculer les inverses est une perte de temps. Une pile est en effet associée à chaque matrice. Les fonctions sont :

   void glPushMatrix( void )

   void glPopMatrix( void )

       qui respectivement empile et dépile la matrice courante. Même s’il n’y a que deux fonctions, il existe bien trois piles différentes (une par matrice). Comme pour les transformations, seule la matrice active peut-être empilée / dépilée (ce qui est logique, puisque que une matrice est empilée pour la sauvegarder avant une transformation).

       Le plus souvent, un objet est caractérisé par ses angles de rotation (ax, ay, az) et sa position (x, y, z). Son affichage est alors du type :

   glPushMatrix() ;
          glTranslate3f(x,y,z) ;
          glRotate3f(ax,ay,az) ;
          glBegin(…)
                 …
          glEnd() ;
   glPopMatrix() ;

Exemple d’utilisation : un soleil avec ses satellites, et les lunes des satellites. Le principe est le même :

-     Dessin du soleil
-     PushMatrix

-     Translation dans le repère local d’une planète
-     Dessin de la planète
-     PushMatrix

-     Translation dans le repère local d’une lune
-     Dessin de la lune

-     PopMatrix
-     PushMatrix

-     Translation dans le repère local d’une  autre lune
-     Dessin de la lune

-     PopMatrix

-     PopMatrix
-     PushMatrix

-     Translation dans le repère local d’une autre planète
-     Dessin de la planète
-     PushMatrix

-     Translation dans le repère local d’une lune
-     Dessin de la lune

-     PopMatrix

-     PopMatrix
-     etc.

              ATTENTION :  la moindre erreur, le moindre oubli d’un glPopMatrix() ou glPushMatrix() peut se révéler désastreux et être en plus difficile à détecter si vous faites se déplacer la caméra.

4.   Depth-Buffer

            En 3D, tout n’est pas affiché : il y a des faces cachées. Comment déterminer ce qui est affiché ? En affichant ce qui est la plus proche de la caméra. C’est le rôle du depth-buffer : il sauvegarde la distance du point actuellement affiché. Si un point plus proche arrive ensuite, il remplace le point et la valeur dans la depth-buffer est changée.

            Le depth-buffer à les même dimensions que le color-buffer. Il s’active grâce à glEnable(GL_DEPTH_TEST) et se désactive par glDisable(GL_DEPTH_TEST). Il ne faut pas, bien sûr, oublier de l’initialiser au début du programme avec la commande : auxInitDisplayMode(… | AUX_DEPTH | …). Il doit être effacé avec glClear(GL_DEPTH_BUFFER_BIT) à chaque fois que la scène est redessinée.

5.    Anti-aliasing

            L’OpenGL est une « state machine » : les paramètres de dessin consistent en une série de variables (très nombreuses, voir les annexes de la spécification OpenGL pour une liste complète).

            La plupart sont modifiables par :

void glEnable( GLenum cap )

void glDisable( GLenum cap )

            Pour l’antialiasing, les paramètres sont : GL_POINT_SMOOTH, GL_LINE_SMOOTH, et GL_POLYGON_SMOOTH. Le but de l’antialiasing est de rendre apparemment plus lisse les lignes en faisant un dégradé sur les contours des points.

6. Culling

            Le culling consiste à ne dessiner que les triangles dont les points sont dans le sens des aiguilles d’une montre (« clockwise ») ou dans l’autre sens (« counter-clockwise »). L’intérêt est l’optimisation : l’objectif est de toujours dessiner les faces extérieures d’un objet dans le sens contraire des aiguilles d’une montre, et de filtrer les faces cachées avec le culling.

            Prenons l’exemple du cube : les faces sont toujours affichées dans le même ordre, peut importe si elles sont visibles ou pas. C’est le depth-buffer qui permet d’afficher les bonnes faces. Cependant le test du depth-buffer est long : il faut calculer la distance de chaque point de chaque face par rapport à la caméra, puis tester le depth pour chaque point (et mettre à jour le depth si le point est plus près). C’est un test beaucoup trop important pour les faces dont on est sûr qu’elles ne seront pas affichées. Il est plus rapide d’éliminer une face entière d’un coup avec le culling.

            L’astuce utilisée par le culling pour savoir de quel coté est vue une face consiste à regarder dans quel sens les points des triangles sont dessinés (en OpenGL tout est décomposé en triangles) :  dans le sens des aiguilles d’une montre ou dans l’autre sens.

            Cette technique est très rapide : en gros il suffit de regarder le signe du produit scalaire de deux cotés d’un triangle. La seule contrainte pour le programmeur est de dessiner dans le bon sens les points.           
Le standard est le CCW (counter-clockwise), que ce soit pour les primitives de glu ou glut, ou encore de 3DS ou Lightwave.
En OpenGL, les fonctions correspondantes sont :

void glCullFace( GLenum mode ) avec mode à GL_FRONT, GL_BACK ou GL_FRONT_AND_BACK, pour indiquer quel coté doit être éliminé au culling.

void glFrontFace( GLenum mode ) avec mode à GL_CW ou GL_CCW pour indiquer quelles sont les faces de devant

Un appel à glEnable / glDisable avec en paramètre GL_CULL_FACE active / désactive le culling.

7. Exemple complet d’utilisation du Depth-Buffer

            Il s'agit d'un simple triangle, comme dans l'exemple précédent, mais coupé par une ligne. J'ai également déssiné un point, mais il ne se voit pas beaucoup.

#include <windows.h>   // Standard Window header required for all programs
#include <gl\gl.h>         // fonctions OpenGL
#include <gl\glaux.h>    // Librarie AUX
#define largeur 250
#define hauteur 250
GLfloat clipHeight;
GLfloat clipWidth;

void setupRC()
{
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f );
            glShadeModel(GL_SMOOTH);
            //glShadeModel(GL_FLAT);
            glFrontFace(GL_CW);
            glEnable(GL_DEPTH_TEST);
}

void CALLBACK Main_part(void)
{
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glBegin(GL_POINTS);
                       glVertex2f(-10.0f,-10.0f);
            glEnd();
            glColor3f(1.0f,0.0f,1.0f);
            glBegin(GL_LINES);
                       glVertex3f(-10.0f,-10.0f,-50.0f);
                       glVertex3f(100.0f,100.0f,50.0f);
            glEnd();
            glBegin(GL_TRIANGLES);                                 //la ligne coupe le triangle et est partiellement cachée
                       glColor3f(0.0f,1.0f,0.0f);
                       glVertex3f(0.0f,130.0f,0.0f);

                       glColor3f(0.0f,0.0f,1.0f);
                       glVertex3f(0.0f,0.0f,0.0f);
                       glColor3f(1.0f,0.0f,0.0f);
                       glVertex3f(130.0f,0.0f,0.0f);
            glEnd();
            glFlush();
}

void CALLBACK Reshape(GLsizei w, GLsizei h) //si il y a un changement de taille de la fenetre ...
{
            GLfloat Near = -100.0f;
            GLfloat Far = 100.0f;
            if (h == 0) h = 1;
            glViewport(0,0,w,h);
            glMatrixMode(GL_PROJECTION);
            glLoadIdentity();
            if (w<=h)
            {
                       clipWidth  = (GLfloat)largeur * h/w;
                       clipHeight = (GLfloat)hauteur;
            }
            else
            {
                       clipWidth = (GLfloat)largeur;
                       clipHeight = (GLfloat)hauteur * w/h;
            }
            glOrtho( -clipWidth/2, clipWidth/2, -clipHeight/2, clipHeight/2, Near, Far);
            glMatrixMode(GL_MODELVIEW);
            glLoadIdentity();
}

void main(void)
{
            auxInitDisplayMode(AUX_SINGLE | AUX_RGBA | AUX_DEPTH);
            auxInitPosition(100,100,largeur,hauteur);
            auxInitWindow("Test");
            setupRC();
            auxReshapeFunc(Reshape);
            auxMainLoop(Main_part);
}

la ligne coupe le triangle et est partiellement cachée

sources et éxécutable

 

Page précédente Page suivante