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
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);
}