OpenGL: principe et astuces


Ce document est issu du groupe de travail iMAGIS du 10/07/98. Il est destiné à donner à la fois des pistes au débutant en 3D et des idées et astuces au programmeur plus confirmé. Il n'est cependant pas complet: le principe consiste plutôt à donner la description des fonctions essentielles, après quoi il faut tirer les fils dans le man pour savoir tout le reste. D'autre part, ce document trahit mes propres limites: je connais davantage le `vieux gl' qu'OpenGL, et je ne connais pas tout loin s'en faut, donc il peut y avoir des erreurs. Les lecteurs sont inviter à en faire part, et à signaler les parties qui demandent éclaircissement ou extension.
Fabrice Neyret, le 30/07/98

Voir aussi: utilisation des NVidia GeForce sous OpenGL, et références et astuces.
Voir aussi: le tutoriel OpenGL d'Antoine Bouthors.

Sommaire


Organisation d'ensemble: les 3 moteurs

OpenGL est une librairie graphique 3D. Cela signifie qu'on lui donne des ordres de tracé de primitives graphiques (facettes, etc) directement en 3D, une position de caméra, des lumières, des textures à plaquer sur les surfaces, etc, et qu'à partir de là OpenGL se charge de faire les changements de repère, la projection en perspective à l'écran, le clipping, l'élimination des parties cachées, d'interpoler les couleurs, et de rasteriser (tracer ligne à ligne) les faces pour en faire des pixels.

OpenGL s'appuie sur le hardware disponible selon la carte graphique. Toutes les opérations de base sont a priori accessibles sur toute machine, simplement elles iront plus ou moins vite selon qu'elles sont implémentées en hard ou pas (pour les fonctions évoluées qui ne font pas partie de la norme OpenGL mais figurent parmi les extensions, il se peut par contre qu'elles ne soient disponibles que sur certaines machines).

La machine OpenGL (graphic engine) se décompose en plusieurs moteurs:

  • le geometric engine s'occupe de la partie purement 3D: triangulation des polygones, changements de repère, projection en perspective à l'écran, clipping (élagage de ce qui sort de l'écran).

  • le raster engine prend en charge la partie 2D: il rasterise les triangles de manière à produire des pixels (que l'on nomme fragments tant qu'ils ne sont pas véritablement inscrits à l'écran), interpole au passage les couleurs et les coordonnées texture, puis élimine les parties cachées par Z-buffer: en fonction de la profondeur z du fragment en cours de tracé et du z actuellement stocké dans le pixel visé, le pixel est ou non modifié (i.e. sa couleur et son z). Ceci constitue le schéma de base, on verra par la suite qu'OpenGL offre de nombreux mécanismes supplémentaires et moyens de contrôle.

  • le raster manager, à qui le pixel est confié pour être tracé à l'écran après d'éventuels traitements supplémentaires (dithering, etc).

    Ouvrir une fenêtre

    A la différence du `vieux GL', OpenGL ne s'occupe pas de l'interface graphique 2D, donc en particulier ni de l'ouverture des fenêtres, ni de la gestion des évènements (e.g. clicks de souris). Ces tâches dépendant de l'OS et de la machine sont laissées à d'autres librairies. Comme il faut bien ouvrir une fenêtre pour dessiner, j'en dis néanmoins quelques mots ici:
    Plusieurs librairies sont disponibles pour faciliter la gestion de l'interface sans descendre dans les bas-fonds de X-window. GLUT en est une simple, fournie avec OpenGL (et elle-même très portable). On inclut alors dans le main() les lignes suivantes: Parmi les initialisations, on positionne les flags utiles, et on indique éventuellement les fonctions chargées de gérer les évènements avec glutMouseFunc(), glutMotionFunc(), glutKeyboardFunc(), glutCreateMenu(). Se ramener au man de GLUT pour les détails.

    Contenu d'un pixel

    En mode true colors, la couleur se code avec 3 octets représentant les composantes rouge, verte et bleue. Mais les pixels peuvent contenir bien plus d'information au delà de la simple couleur: on utilise généralement aussi une profondeur z (utilisée pour le Z-buffer) et une opacité alpha (d'où la notation RGBA). De plus, ces valeurs sont doublées en mode double buffer, lequel permet de faire des animations fluides en laissant visible l'image précédente pendant que l'on trace la nouvelle dans le back-buffer. Comme ces informations diverses réfèrent aux mêmes pixels, on parle de `plans', superposés comme des calques. On dispose également d'un plan de pop-up qui permet de tracer des menus ou des pointeurs sans altérer le contenu de l'image, de plans de stencil dont nous parlerons plus loin qui permettent de masquer certaines parties de l'image, voire d'une image de fond pour les machines qui ont assez de mémoire. Le nombre de bits alloué aux divers plans est paramètrable, la quantité disponible dépendant de la mémoire de la carte graphique. La commande shell privilégiée Xscreen permet de modifier la configuration. Par défaut, sur les machines à peu de mémoire, passer en double buffer impose de prendre sur les autres plans, notamment alpha et z. Sur O2, il est recommandé de changer cette valeur par défaut inutilement économe pour le mode complet 32+32; pensez a vérifier si ça a été fait sur votre station.

    Description d'une primitive géométrique

    Toute primitive surfacique 3D est décomposée en triangles par OpenGL. Le triangle, la ligne et le point sont donc les seules primitives géométriques traitées par le hardware, ce qui ramène toutes les interpolations au cas linéaire (facile à traiter de façon incrémentale en hard, d'où cette limitation aux triangles, mais qui peut produire un aspect légèrement `anguleux'). On spécifie un triangle de la façon suivante: OpenGL permet de spécifier des primitives plus complexes, qui seront décomposées: quadrilatères avec GL_QUADS, polygones avec GL_POLYGON. On peut également ne tracer que les sommets (GL_POINTS), ou que les contours (GL_LINE_LOOP).
    NB: on gagne à indiquer à OpenGL qu'il peut éliminer les `faces arrières' (i.e. que l'on voit de dos), avec glEnable(GL_CULL_FACE), sauf si bien sûr on doit vraiment les voir (transparence, facettes à deux faces type feuilles d'arbre). Comme pour tous les attributs, l'indication reste valable jusqu'à nouvel ordre.

    Noter que la syntaxe de glVertex(), comme celle de tous les attributs (couleur, normale, etc), est polyforme:

  • on peut fournir 2 à 4 composantes (la 4ième correspond à la coordonnée homogène pour des positions, à l'opacité alpha pour des couleurs, et on peut se passer de la 3ième coordonnée pour les positions si l'on fait des tracés 2D, i.e. dans un plan parallèle à l'écran);
  • on peut utiliser des floats, des doubles, des shorts, des ints pour les coordonnées (suffixe f,d,s ou i);
  • on peut citer explicitement les coordonnées, ou passer par un vecteur (sur-suffixe v).

    Primitives non géométriques

    A côté des primitives géométriques, OpenGL permet de manipuler des blocks de pixels, notamment à l'aide de la fonction glDrawPixels(). On peut également récupérer en mémoire de tels blocks avec glReadPixels(), ou les recopier d'une zone de l'écran à une autre avec glCopyPixels().

    Les divers repères

    Plusieurs repères interviennent dans la description d'une scène; on ne donne pas toutes les coordonnées dans un unique repère monde. Ceci permet de changer facilement de point de vue sans modifier la description des objets, ou de changer la position des lumières, ou encore de modifier l'orientation des objets. Ceci permet en outre de réutiliser facilement des descriptions de parties d'objets par simple changement de repère.

  • Pour positionner un objet, on passe par la matrice MODELVIEW: on peut alors effectuer le tracé dans un repère local, et les coordonnées subiront ensuite la pile de transformations indiquée (en remontant l'ordre des déclarations). Cette pile est formelle, car en fait on construit une matrice 4x4 unique au fur et à mesure, laquelle s'applique ensuite aux coordonnées.
    Il existe cependant bien une pile: à tout moment on peut faire glPushMatrix() ou glPopMatrix() pour effectuer une transformation temporaire. Ainsi si l'on s'est placé dans le repère local à un bonhomme, et que l'on sait dessiner un bras dans un repère local, on pourra procéder comme suit:

  • Pour décrire la caméra, on passe par la matrice GL_PROJECTION.
    On y décrit tout d'abord le type de projection: Pour la projection orthographique on indique le domaine [xmin,xmax]x[ymin,ymax] des coordonnées visibles, pour la projection perspective on donne l'angle d'ouverture et le ratio largeur/hauteur; dans les deux cas on précise le domaine de validité de la profondeur z (on verra plus tard que ça a une importance sur la précision).
    Puis on peut orienter la caméra soit en visant un point avec gluLookAt(position,visée,haut), soit en procédant par rotations et translations. Voir le manuel de ces fonctions ou les exemples donnés plus loin pour les détails de mise en oeuvre.

  • Pour déplacer les lumières, on utilise la matrice MODELVIEW comme pour un objet. Le point indiqué à glLightf() est transformé au moment où l'on appelle cette fonction. On peut alors réinitialiser MODELVIEW et s'en servir pour positionner les objets.

  • On verra bientôt que l'on dispose également d'une matrice GL_TEXTURE pour les textures, qui permet notamment de calculer des textures projetées. Selon le même principe que précédemment, on stocke alors comme valeurs `locales' dans les coordonnées texture (u,v,s,t) les coordonnées du point 3D qui se projette là, et la matrice décrit la transformation (par exemple projection orthographique ou perspective).

    Attributs d'une face

    Il existe plusieurs façons de spécifier l'apparence d'une facette:

  • couleur de la face: on utilise glColor3f(r,g,b). On peut également préciser l'opacité en ajoutant un paramètre alpha. (A noter que coder la transparence par une seule valeur et non par un triplet Ar,Ag,Ab est assez pauvre: un objet rouge derrière un transparent vert apparaît bien atténué, mais toujours rouge ! On verra en traitant du mélange des pixels des moyens de faire mieux, qui sont cependant peu utilisés).
    On gagne à préciser au système qu'il est inutile d'interpoler, avec glShadeModel(GL_FLAT).

  • couleur aux sommets, à interpoler sur la face: on redéfinit simplement cette couleur avec glColor() pour chaque sommet, juste avant le glVertex(). Mieux vaut repréciser auparavant avec glShadeModel(GL_SMOOTH) que l'on souhaite interpoler les valeurs.

  • modèle d'illumination (Phong) en fonction de l'orientation par rapport aux lumières. Un tel modèle nécessite la définition d'une matière composée d'une couleur ambiante (celle qui apparaît dans l'ombre), une couleur diffuse (celle qui apparaît du côté éclairé, souvent la même), une couleur spéculaire (celle des reflets, blanche pour du plastique), et un coefficient de rugosité (qui contrôle l'épaisseur de la tache spéculaire). Comme le suggère le flag GL_FRONT_AND_BACK, on peut définir une matière différente sur les deux faces. (A noter que l'on peut aussi définir une couleur d'émissivité avec le flag GL_EMISSION).
    On peut alors mettre en place jusqu'à 8 lumières, dont on peut spécifier de nombreux attributs par glLightfv(GL_LIGHT0, attribut, vecteur): tout d'abord la position (GL_POSITION) et la couleur (dont on donne séparement les composantes ambiante, diffuse et spéculaire, ce qui permet de faire des choses peu physiques, comme des sources qui ne génèrent pas de reflets, ou que des reflets, ou qui ne contrôlent que la lumière ambiante), mais aussi l'atténuation en fonction de la distance, et de l'angle (pour un spot). Une lumière est activée par glEnable(GL_LIGHT0).

    L'illumination tient compte de la direction de la lumière et de l'observateur par rapport à l'orientation de la facette, il faut donc préciser cette dernière via glNormal() à chaque sommet, avant le glVertex(). On peut aussi laisser OpenGL estimer tout seul les normales par glEnable(GL_AUTO_NORMAL), mais c'est coûteux et moins précis. On peut également lui demander de normaliser les normales par glEnable(GL_NORMALIZE), ce qui est notamment utile quand on spécifie une déformation de l'objet (ne serait-ce qu'un scaling), mais il faut bien comprendre le fait que cela entraîne un calcul supplémentaire par sommet, lequel calcul inclut l'évaluation d'une racine carrée, sachant que chaque sommet est redéfini pour toutes les faces auxquelles il appartient (en général 6).

  • Texture plaquée sur la face. On décrira plus loin les diverses modalités qui contrôlent les textures. De toutes façons on sépare la description du motif de la spécification de son plaquage, lequel est donné en fixant les valeurs des coordonnées texture (u,v) à l'aide de glTexCoord2f(u,v) à chaque sommet, avant le glVertex(). On fait ainsi la correspondance entre la position sur la `tapisserie' et la position dans le monde 3D (ce qui suggère tous les problèmes qui peuvent apparaître dans les coins, pour les surfaces non développables, etc). On verra plus tard que l'on peut donner jusqu'à 4 coordonnées de textures, ce qui s'utilise dans diverses circonstances (textures projetées, textures d'environnement, textures solides...).

    Exemples de programmes OpenGL+GLUT

  • exemple simple appli OpenGL animé (92 lignes).
  • squelette simple appli interactive OpenGL+GLUT (172 lignes).
  • squelette complet appli interactive OpenGL+GLUT (300 lignes).

    Mélange des attributs d'une face

    On souhaite pouvoir combiner ces divers modes, notamment en tenant compte de la transparence que l'on a spécifiée au niveau de la face, des sommets, ou des pixels de texture. OpenGL offre de nombreuses modalités de combinaison de ces attributs.
    Il faut bien voir que l'on est ici à moitié dans les territoires du geometric engine et du raster engine: le premier choisi les valeurs qu'il faut stocker aux sommets et fait les transformations nécessaires (ainsi l'illumination n'est évaluée qu'aux sommets, et constitue alors simplement une couleur à interpoler sur la face comme dans le cas le plus simple; de même la machine peut calculer à ce stade les (u,v) aux sommets pour simuler le reflet de l'environnement), le second interpole les attributs retenus et en fait le mélange comme spécifié pour produire les fragments.

    Couleur et matière
    On peut utiliser la couleur de la face ou la couleur interpolée à partie des sommets pour moduler l'illumination du matériau. Pour cela, on indique lequel des paramètres du modèle d'illumination est contrôlé par la couleur de la surface à l'aide de glColorMaterial(GL_FRONT_AND_BACK,param).
    NB: le paramètre spécial GL_AMBIENT_AND_DIFFUSE permet de faire varier ces deux composantes en même temps.

    Couleur et texture
    On passe par la fonction glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,mode). On considère que la texture écrase toute autre couleur si le mode vaut GL_REPLACE, qu'elle correspond à un vernis coloré qui altère la couleur de la surface (GL_BLEND), qu'elle indique la lumière et l'ombre qui atteint la face en chaque point (GL_MODULATE), qu'elle contient une couleur qui s'additionne à celle de la face (GL_ADD), ou encore qu'elle contient une couleur dont l'opacité alpha contrôle ce qu'il apparaît de la couleur de la face (GL_DECAL). Il existe d'autres attributs de contrôle (notamment une couleur constante), et de nombreuses variantes selon le nombre de plans fournis dans la texture, il faut donc se référer au manuel pour plus de détails.

    On voit ainsi que l'on peut se servir des textures pour creuser une surface (via la transparence), ou pour donner l'apparence d'une illumination détaillée. En fait avec un peu de travail on peut même s'en servir pour obtenir des ombres (textures en Z) et simuler du relief (bump) comme on le verra plus loin.

    Limitations

    A ce stade, on tombe sur deux limitations conceptuelles d'OpenGL:
  • même s'il y a de nombreuses combinaisons possibles entre couleur de la surface et de la texture, on ne peut utiliser qu'une texture à la fois. C'est une contrainte très dure, qui force à exécuter plusieurs passes pour construire des images complexes (on parlera plus loin du multipass).
  • le Z-buffer s'accommode très mal d'une transparence: même si plusieurs couches transparentes ont contribué à la couleur d'un pixel, on ne peut mémoriser que le z d'une seule d'entre elles. Il faut alors prendre garde à ne tracer ces couches que de l'arrière vers l'avant où de l'avant vers l'arrière (et indiquer ce choix à la machine, voir plus bas), ce qui peut nécessiter de procéder à un tri des faces, lequel peut être coûteux. Faute de quoi le raster engine n'a aucun moyen de savoir comment s'intercale la nouvelle face transparente parmi les anciennes, et il fera un choix arbitraire, généralement en considérant le z comme si la face était opaque: si le z du fragment est plus grand que celui en place dans le pixel on ne fait rien, sinon on trace le pixel en combinant les couleurs (en tenant compte de la transparence, via une opération de blend dont on parlera plus loin), et on remplace l'ancien z par le nouveau.
    C'est souvent très fâcheux, notamment quand on utilise un texture d'opacité pour creuser une surface: du point de vue du raster engine, tous les fragments du triangle existent, et ont une couleur et un z auxquels le traitement standard du Z-buffer s'applique, même s'il se trouve que certains fragments sont totalement transparents (et se retrouvent néanmoins masquant). Un palliatif commode est fourni par OpenGL avec la fonction glAlphaFunc(GL_GREATER,0.), qui permet de décréter un seuil sur l'opacité en dessous duquel le tracé du fragment courant est abandonné. Si l'on utilise que des transparences en 0 ou 1, on obtient alors toujours un résultat correct sans précaution particulière.

    D'autre part, le codage des valeurs sur un certain nombre de bits engendre plusieurs limites pratiques:

  • Selon la mémoire disponible, les z sont stockés sur 16 ou 24 bits. Ceci engendre des artefacts dès que 2 facettes sont proches, où des pixels qui ne devraient pas apparaissent au devant, ou clignotent lors de l'animation. Pour profiter au mieux de la résolution en z, il faut impérativement tenir compte de la dynamique de cette valeur dans la scène, ce qui se fait en positionnant au mieux les plans de clipping avant et arrière qui définissent l'intervalle des z, que l'on précise dans glOrtho() ou gluPerspective().
    NB: le codage des z n'est pas linéaire, ce qui permet de bénéficier de plus de précision en z pour les objets proches. A noter que lorsqu'on modifie des flags (e.g. attribut rendu, mode de texture), on est jamais certain que c'est exactement le même z qui sera tracé, ce qui peut parfois entraîner des problèmes (e.g. lors du tracé d'une ligne sur un plan). La fonction glPolygonOffset() est un hack qui permet de résoudre certains d'entre eux, comme le tracé de lignes sur une surface.
  • les valeurs (couleur, transparence) qui sortent de l'intervalle [0,1] sont `clampées' au moment d'être stockées (i.e. les valeurs sortant de l'intervalle sont ramenées à la borne la plus proche), mais pas forcément pendant les traitements.
    - Exemples de cas défavorables: si un des sommets est à l'ombre (N.L<0) et les deux autres à la lumière, le dégradé commencera néanmoins dès le sommet, car la valeur négative a été ramenée à 0. Si l'on veut amplifier la modulation d'une texture sombre par l'utilisation de couleurs aux sommets supérieures à 1, ça ne marche pas non plus car la valeur est clampée à 1.
    - Exemples de cas favorables: on peut utiliser des sources de lumières dont l'intensité est quelconque, voire négative; le clamping n'intervient qu'au niveau de l'intensité totale stockée à chaque sommet. L'accumulation buffer (cf plus bas) dispose d'une résolution plus importante pour stocker les valeurs (car utiliser de faibles pondérations entraîne une perte de précision).

    Mélange des pixels

    Il s'agit ici de préciser comment la couleur d'un fragment (une fois que l'on sait qu'il doit être tracé) se mélange à celle du pixel déjà en place, notamment pour tenir compte de la transparence; on est donc purement dans le territoire du raster engine. Quand on ajoute une couche colorée transparente C2=RGBA2 devant une couche C1=RGBA1 (l'image déjà présente à l'écran), la couleur résultante est C2*A2+C1*(1-A2), et si on l'ajoute derrière, C2*(1-A1)+C1 (à noter que dans le premier cas, on a pas besoin de stocker véritablement les opacités dans le buffer d'image, alors qu'il le faut dans le second cas). OpenGL offre en fait un contrôle plus souple du mélange: on spécifie un coefficient multiplicateur pour C1 et C2 avec glBlendFunc(), et une fonction de mixage glBlendEquation(). Dans le cas classique on aura alors Ces paramètres correspondent à un tracé des faces transparentes triées de l'arrière vers l'avant (on dessine en estompant ce qui est déjà présent), mais l'on peut tout aussi bien additionner 2 images (e.g. image aux rayons X), les multiplier (pour simuler des filtres transparents), les soustraire, passer par des opérateurs min et max, etc. Les coefficients peuvent désigner l'opacité de la source ou de la destination, ou 1-opacité, mais aussi leur couleur, ou 1-couleur, voire une opacité ou une couleur constante donnée par glBlendColor() (typiquement pour faire du fondu-enchaîné entre images). A noter que l'on peut ainsi calculer la somme des carrés de deux images, en utilisant glBlendFunc(GL_SRC_COLOR,GL_DST_COLOR), et l'on verra avec les lookup tables que l'on peut ensuite en prendre la racine carrée dans la foulée.

    L'accumulation buffer
    Il s'agit de l'autre technique pour mélanger les images, qui consiste à additionner une série d'images entières par glAccum(GL_ACCUM,1./n), avec éventuellement des pondérations, sans tenir compte du z ou de l'alpha mais purement des couleurs. On utilise notamment cette fonctionnalité (lourde, car faisant intervenir tous les pixels de l'image) pour traiter la profondeur de champ, le flou de bougé ou l'antialiasing, en bougeant légèrement la position de la caméra ou des objets entre chaque image.

    Les stencils

    Le stencil plane est un plan supplémentaire qui permet d'associer un flag à chaque pixel. OpenGL fournit des fonctions pour positionner ces flags, généralement en même temps qu'un premier tracé, et pour les tester au cours des tracés suivants. Cette fonctionnalité est activée par glEnable(GL_STENCIL_TEST). Ceci sert notamment à masquer une partie de l'image: pour un simulateur de vol, on souhaite ne tracer le cockpit qu'une fois, et ensuite le paysage ne doit être tracé que dans les fenêtres. En dessinant le cockpit on demande alors à OpenGL de mettre à 1 le stencil plane là où l'on aura dessiné, par
    glStencilFunc(GL_ALWAYS,1,1); glStencilOp(,,GL_REPLACE),
    puis on indique qu'il ne faut désormais plus tracer que dans les zones où le stencil est à 0, par glStencilFunc(GL_EQUAL,0,1); glStencilOp(GL_KEEP,GL_KEEP,GL_KEEP).

    Ces deux fonctions permettent en fait des opérations plus générales: le plan de stencil comporte plusieurs bits (souvent 8), lesquels peuvent être considérés soit comme des flags indépendants, soit comme formant un nombre entier. L'opérateur glStencilFunc(cmp,ref,mask) peut alors tester si la valeur du stencil est inférieure, égale, ou supérieure à un seuil avant d'autoriser le tracé, et on peut préciser un masque pour isoler une partie des bits. La fonction glStencilOp() permet d'indiquer comment affecter le stencil selon que le fragment courant a été écarté par le test, qu'il l'a franchi mais a été écarté par le Z-buffer, ou qu'il a pu être tracé à l'écran. Les opérations disponibles consistent à mettre à zéro (GL_ZERO) ou à une valeur (GL_REPLACE) de référence (donnée via glStencilFunc()), incrémenter ou décrémenter (GL_INCR, GL_DECR) la valeur de stencil considérée comme un nombre entier, ou inverser (GL_INVERT) les bits (autorisés par le masque indiqué par glStencilMask()).

    Ceci permet de réaliser de nombreux effets: on peut ainsi repérer et inscrire dans le stencil une zone d'ombre (par exemple en effectuant la projection d'un objet sur le plan du sol), et l'on tracera ensuite d'une couleur différente la zone extérieure et la zone intérieure; on peut réaliser un miroir plan en ne traçant la scène renversée que dans la limite du miroir; on peut aussi créer une zone circulaire autour de la souris où la scène apparaît en filaire plutôt qu'en faces cachées (ou l'inverse); on peut compter le nombre de primitives se projetant en chaque pixel (en incrémentant le stencil pour chaque fragment généré, qu'il passe ou non le test du Z-buffer), etc. On pourrait également utiliser les stencils pour tracer des maillages en face cachée (le tracé de lignes sur des polygones ayant le même z posant de gros problèmes d'imprécision): pour chaque face, on tracerait d'abord le contour en positionnant à 1 le stencil (si le fragment franchit le test du Z-buffer). on tracerait ensuite le polygone en n'autorisant que les zones à stencil nul, puis on effacerait les 1 du stencil pour ne pas affecter les tracés ultérieurs, en redessinant le contour. Cette méthode élimine proprement les conflits de z, par contre elle est inutilement coûteuse puisque glPolygonOffset() permet de traiter directement ce problème spécifique.

    D'autres masques

    Il existe deux autres façons de cacher une partie du tracé:
  • pour se restreindre à une zone rectangulaire de l'écran (e.g. une sous-fenêtre, ou pour laisser une bordure), on utilise glScissor(). Il faut penser à activer le test, avec glEnable(GL_SCISSOR_TEST). Mais il faut prendre conscience que ce test intervient au niveau du raster engine, et donc que tous les fragments sont générés (i.e. le clipping continue à considérer la fenêtre en vraie grandeur): si la zone autorisée est toute petite, c'est du gaspillage.
  • pour éliminer une partie de la scène 3D, on peut jouer sur les plans de clipping avant et arrière dont on a déjà parlé. De même, OpenGL effectue un clipping sur les quatre plans qui constituent la pyramide de vision (en dehors de laquelle les primitives se projettent hors de l'écran). On peut en outre préciser plusieurs plans de clipping supplémentaires arbitraires avec
    glClipPlane(i,params[]); glEnable(GL_CLIP_PLANEi). Ceci peut permettre d'isoler un objet, mais aussi de tracer différemment deux zones de l'espace: on fait par exemple un premier tracé en faces cachées à droite du plan, puis on retourne le plan de clipping et on fait le reste du tracé en filaire (lequel plan peut même couper un objet en deux).

    [Attention je connais mal l'aspect qui suit en OpenGL, j'ai donc pu dire des bêtises.]
    Quand on réalise un logiciel de type modeleur plutôt que de chercher à produire un rendu réaliste, on peut se permettre d'utiliser une palette de couleurs plutôt que le champ continu du true colors. Les 3*8bits peuvent alors être utilisés comme autant de flags, ce qui revient à considérer que l'image est constituée de calques superposés. On peut par exemple utiliser 3 calques de 8 bits, qui autorisent chacun 256 couleurs. Pour un modeleur où les objets apparaissent en filaire, on peut aussi considérer 24 calques monochromes. On peut alors librement tracer ou effacer sélectivement un de ces calques sans toucher aux autres, ce qui permet de ne rafraîchir que l'objet de la scène que l'utilisateur est en train de modifier, ce qui représente un gain considérable en nombre de primitives à tracer. On entre dans ce mode avec glEnable(???) (par opposition à ???), et on donne les couleurs avec glIndex() à la place de glColor(). On joue sur le masquage des bits avec glIndexMask() (on pourrait aussi le faire en mode true colors avec glColorMask()). Attention, de nombreuses opérations décrites dans ce document ne fonctionnent plus dans ce mode (transparence, mélanges, ...).

    Du fragment au pixel: ordre des tests

    Comme on l'a vu, un fragment d'un triangle subit de nombreux tests avant d'affecter le pixel correspondant. Pour éviter les ambiguïtés, il est important de se rappeler que ceux-ci interviennent dans l'ordre suivant:
  • scissor test: le fragment n'est gardé que s'il tombe dans la boîte.
  • alpha test: le fragment n'est gardé que si son opacité est au dessus du seuil.
  • stencil test: le fragment n'est gardé que si la valeur du stencil du pixel franchit le test. S'il échoue, on peut laisser une trace spécifique dans le stencil plane.
  • z test: le fragment n'est gardé que s'il est accepté par le Z-buffer, c'est à dire (en général) qu'il est devant le contenu précédant du pixel (i.e. son z est inférieur). S'il échoue, on peut laisser une trace spécifique dans le stencil plane.
  • alors seulement le fragment peut être tracé (et éventuellement laisser une trace spécifique dans le stencil plane). A ce stade interviennent les opérations de mélange faisant intervenir la transparence, dont on a parlé plus haut.

    Comme tous les autres, le test du Z-buffer doit être activé par glEnable(DEPTH_TEST) pour être mis en oeuvre, et il est paramétrable: glDepthFunc() permet de choisir l'opération à tester pour retenir un fragment lors du test du z.

    Multipass rendering

    Les restrictions d'OpenGL, qui découlent notamment de celles du hardware, introduisent de nombreuses limites, par exemple sur le nombre de lumières, mais surtout sur le nombre de textures: on ne peut en utiliser qu'une à la fois, alors que les différents modes de mélange permettent de moduler pratiquement chaque attribut à l'aide des textures ! De même on ne peut pas savoir directement si l'on se situe dans une zone d'ombre, la réflection dans les miroirs n'est pas prise en compte, etc.

    Une image complexe sera donc rendu en plusieurs passes successives, chacune modifiant ou complétant le résultat des précédentes. Ainsi les opérations de mélange permettent de tracer une première couche de surface, puis d'autres qui affectent la teinte (vernis) ou la luminosité (ombres fines), puis ajoutent des reflets, etc. Dans le même esprit, on peut générer du pseudo-relief (bump mapping) en soustrayant deux textures identiques représentant le relief légèrement décalées dans le sens de la lumière (de manière à réaliser un opérateur de type Sobel). On ajoute la couleur au relief en multipliant le résultat par une texture de couleur, le tout prend donc trois passes. Les stencils permettent de séparer des régions qui devront recevoir des traitements différents (par exemple dans et hors des ombres).

    Le multipass permet donc de regagner de la souplesse pour dépasser les limites de ce que le hardware peut gérer en même temps. Cependant multiplier les passes coûte cher, même si certaines ne concernent que quelques faces. Il faut donc veiller à éviter tout traitement inutile (on verra bientôt comment).

    Optimisations

  • Toute fonctionnalité utilisée coûte cher ! D'une part par le traitement et les tests qu'elle impose, mais aussi par le simple fait qu'il faut alors gérer des données supplémentaires, lesquelles doivent être générées, stockées, transmises et interpolées. Il faut se méfier de la valeur par défaut des flags, et désactiver par glDisable() tout ce dont on n'a pas besoin, car OpenGL ne fait aucun test de cohérence: si l'on utilise une texture qui remplace totalement la couleur de la surface, cette couleur est néanmoins interpolée à partir des valeurs aux sommets, et le coûteux modèle d'illumination sera lui aussi inutilement évalué aux sommets (autant de fois qu'il y a de lumières) si le mode LIGHTING est actif. De même, l'atténuation due au brouillard est calculée dès que le mode FOG est actif (peut-être l'est-il par défaut sur certaines machines), quand bien même sa densité serait nulle ! Dans le même esprit, la couleur aux sommets est interpolée par défaut, même si l'on a fourni qu'une seule couleur pour toute la facette.
    Typiquement, lors du multipass, il faut repérer à quels moments on a réellement besoin de tester le z ou le alpha, et quelles sont les passes qui nécessitent vraiment le calcul de l'illumination (dans la négative, on peut aussi se passer de normaliser les normales).

  • Inversement, certaines fonctions permettent de réduire les calculs: Ainsi, l'évaluation de l'illumination coûte moins cher lorsque les lumières sont à l'infini (4ième coordonnée nulle). Dans un autre registre, on peut pratiquement diviser par deux le nombre de faces à tracer en éliminant celles qui tournent le dos à la caméra avec glEnable(GL_CULL_FACE).

  • Il faut utiliser autant que possible les instructions OpenGL plutôt que de faire les opérations à la main (e.g. calcul matriciel), passer par les primitives les plus simples et spécifiques (triangles plutôt que polygones), et les grouper dans un seul glBegin()..glEnd() (séries de faces, de lignes, de points). En outre, OpenGL introduit des primitives adaptées aux maillages, comme les GL_TRIANGLE_STRIP où chaque triangle partage deux sommets avec le précédent, ce qui évite de répéter inutilement l'évaluation des attributs au sommet (ce qui se produit quand on doit mentionner plusieurs fois le même sommet, et qui peut être coûteux quand il s'agit de l'illumination ou des normales).
    Quand on doit utiliser plusieurs fois le même objet (instances multiples, mouvement rigide, multipass), OpenGL prévoit l'utilisation de display lists, qui constituent une version précompilée d'une description 3D: la génération automatique éventuelle des normales et des (u,v) est effectuée ainsi que les changements de repères (rotations, translations, scaling entre membres de l'objet), et toutes les données sont converties au format interne une bonne fois. On peut ensuite afficher la liste autant de fois que nécessaire sans recalculs inutiles. On construit une liste en encadrant une série d'ordres OpenGL classiques par glNewList(i,GL_COMPILE) et glEndList(), puis on utilise glCallList(i) pour l'afficher.

  • Il faut faire attention à la saturation du pipeline graphique: les instructions graphiques sont exécutées de manière asynchrone, mais le processeur doit attendre si on a rempli la pile plus vite qu'elle est traitée. Il faut donc en profiter pour intercaler les calculs et l'affichage. Ainsi, effacer l'écran est une opération lourde. On gagne donc à effectuer immédiatement après tous les calculs nécessaires pour préparer le tracé, pendant que l'effacement se réalise et avec un peu de chance se termine, le pipeline étant alors près à recevoir les instructions de tracé au moment où on les donne. Si l'on avait effectué les calculs puis le glClear() et l'affichage, il y aurait eu un temps où le moteur graphique n'avait rien à faire pendant que le processeur calculait, puis un temps où le processeur était en attente pendant que le moteur graphique avait à venir à bout d'un effacement et d'un tracé.

  • Quand on souhaite optimiser une application graphique, il faut commencer par déterminer où se situe le goulot d'étranglement: dans les calculs, dans le geometric engine, ou dans le raster engine.
    - si désactiver l'affichage (par exemple en changeant les glVertex() en glColor()) ne réduit pas le temps, le goulot est dans la partie calcul;
    - si activer ou désactiver l'illumination locale change la vitesse, le goulot se situe probablement dans la partie 3D;
    - si supprimer les textures, le test des z ou le mélange accélère l'affichage, le goulot se situe dans la partie 2D.

    Les textures

    On a vu plus haut comment régler l'apparence des textures et leur combinaison à la couleur d'une surface. L'autre moitié du problème consiste à spécifier la façon dont une texture se plaque sur la surface, ce dont on va traiter ici. Mais avant tout, il faut charger l'image en mémoire.

    Charger une image
    La lecture depuis le disque d'une image au format RGB se fait avec iopen(), puis en lisant chaque ligne avec getrow(). Le format n'étant pas le même en mémoire, il faut réorganiser les informations de manière à obtenir une table de triplets RGB:

    On déclare que cette image doit être utilisée comme texture courante par glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,l,h,0,GL_RGBA,GL_UNSIGNED_BYTE,table). Transférer ainsi une image vers la mémoire texture coûte cher, il serait assez prohibitif d'avoir à procéder ainsi à chaque fois qu'on change de texture pour un autre objet de la scène. OpenGL permet de créer une banque de textures, à concurrence de la mémoire disponible, que l'on peut constituer une fois pour toute. On encadre la définition de la texture (y compris les paramètres dont nous parlerons plus loin) d'une déclaration permettant de donner un numéro à une texture, par glBindTexture(GL_TEXTURE_2D,i) et glBindTexture(GL_TEXTURE_2D,0). A l'utilisation, il suffit alors de mentionner à nouveau glBindTexture(GL_TEXTURE_2D,i) pour rendre cette texture courante, sans qu'un nouveau transfert soit nécessaire. Il ne faut bien sûr pas oublier d'activer le rendu des textures, par glEnable(GL_TEXTURE_2D).
    NB: si l'on souhaite remettre à jour une texture qui évolue au cours du temps, on gagne énormément si l'on peut se contenter de transférer la petite partie qui a changé, avec glTexSubImage2D(GL_TEXTURE_2D,0,x,y,l2,h2,GL_RGBA,GL_UNSIGNED_BYTE,table2).

    Paramètres de la texture
    OpenGL permet de régler de nombreux paramètres régissant la façon dont la texture se répète, les traitements à effectuer quand on la voit de près ou de loin, etc. Se référer au manuel pour plus de détails.
    A titre d'exemple, on fait répéter le motif ou on ne le fait apparaître qu'une seule fois en utilisant le flag GL_REPEAT ou GL_CLAMP pour chacune des deux coordonnées textures u et v repérées par les flags GL_TEXTURE_WRAP_S et GL_TEXTURE_WRAP_T dans la commande glTexParameteri(GL_TEXTURE_2D,coord,flag). D'autre part, on peut choisir d'interpoler ou pas la texture (flag GL_LINEAR ou GL_NEAREST) lorsqu'on est loin ou proche (cas TEXTURE_MIN_FILTER ou TEXTURE_MAG_FILTER) via la fonction glTexParameteri(GL_TEXTURE_2D,cas,flag).
    D'autres paramètres permettent de régler finement ce qu'il se passe au bord du motif, de gérer les niveaux de détail par MIP-mapping pour traiter le cas des textures éloignées en évitant l'aliasing, ou encore de traiter des textures plus grandes que la mémoire décomposées en plusieurs tiles.

    Plaquage de la texture
    Pour appliquer une texture sur une surface, la méthode consiste à indiquer où les sommets définissant la surface tombent dans l'espace (u,v) de la texture. Par interpolation des coordonnées texture sur les facettes, on obtient alors la projection sur toute la surface (on devine cependant tous les problèmes de type tapisserie qu'occasionne cette technique. On peut juste se consoler en se disant que tout le monde subit le même problème ! ).
    Les coordonnées texture d'un sommet s'indiquent par glTexCoord2f(u,v) juste avant le glVertex(). Il existe cependant des méthodes pour générer automatiquement les (u,v) dans certains cas spécifiques: quand la surface est obtenue automatiquement par subdivision d'un patch de Bézier ou de NURBS, quand la texture est directement corrélée à l'altitude ou à la direction de vue pour certains effets de visualisation, ou quand on se sert de la texture pour simuler les reflets de l'environnement dont on parlera bientôt.

    Textures non 2D
    Pour OpenGL, une coordonnée texture est simplement un attribut supplémentaire des sommets, au même titre que la couleur, et pour lui interpoler 1 ou 4 valeurs revient au même. Cette valeur sert ensuite d'index dans une table, et là encore définir une table à une seule ou à 4 dimensions n'est pas fondamentalement différent de ce qu'on fait pour 2. De même les opérations de répétition ou de filtrage se généralisent sans problème. Ainsi donc, OpenGL prévoit jusqu'à 4 coordonnées (u,v,s,t) de texture; libre aux utilisateurs d'y trouver un usage.
    Il y a plusieurs applications intéressantes: pour les textures 1D, qui sont d'autant plus souples qu'elles ne supposent aucun paramètrage de la surface, on peut par exemple considérer u comme un potentiel à la surface, ou encore comme une altitude, et représenter en couleur les diverses taches de potentiel, ou les isovaleurs. Les textures 3D permettent d'implémenter en Z-buffer les textures solides comme le bois et le marbre, matériau dans lequel on taille les objets (les (u,v,s) sont alors une transformation affine des coordonnées géométriques). On peut également se servir des textures 3D pour visualiser des volumes style données scanner, en taillant des tranches plus ou moins opaques dans ce volume de texture. Ces modes s'activent simplement en précisant 1D, 3D ou 4D à la place de 2D dans les fonctions que l'on a vu plus haut.

    Textures projetées
    Comme on l'a vu plus haut, OpenGL fournit une matrice 4x4 permettant au geometric engine de transformer les coordonnées de texture (u,v,s,t) comme on le fait avec les changements de repère pour les coordonnées géométriques. Ceci permet par exemple de texturer une surface en simulant une projection parallèle ou conique depuis une diapositive: on duplique dans les (u,v,s,t) des sommets les coordonnées géométriques, et on positionne la matrice de sorte à réaliser une projection sur le plan de la diapo.

    Textures d'environnement
    On peut simuler des objets réfléchissants avec les textures! Le principe consiste à préparer un image de l'environnement entourant un objet, en supposant que cette image se situe sur la surface intérieure d'une sphère infiniment grande centrée sur l'objet. Dans un tel cas, deux rayons réfléchis parallèles voient le même point de cette sphère (i.e. celui pointé par la direction du rayon), ce qui revient à dire que seule la direction de réflection importe et non la position. Pour faire de telles textures de reflets, OpenGL va donc stocker en chaque sommet de l'objet le (u,v) que l'on y voit sur la sphère, simplement en calculant la direction de réflection qui est le symétrique de la direction de l'observateur par rapport à la normale au sommet, ces deux informations étant connues au moment où est effectué le glVertex(). Ce plaquage particulier est obtenu par:

    Textures d'ombre
    On peut obtenir des ombres portées grâce au textures! Le principe consiste à construire une texture de z, représentant la distance de ce que voit une lumière dans la direction du point où se projette chaque pixel de texture. Une telle image s'obtient en effectuant un rendu préalable par Z-buffer avec le point de vue positionné au lieu de la lumière. Pour le rendu final, on duplique dans les (u,v,s,t) des sommets les coordonnées géométriques, et on positionne la matrice de texture pour effectuer une projection perspective sur la lumière, de façon à retomber sur l'image préalable. En appliquant cette transformation, le geometric engine transforme ainsi les (u,v,s,t) en les coordonnées `écran' vues depuis la lumière dont seul la distance z' nous intéresse. Au moment du rendu, le raster engine compare le z' du fragment à celui stocké dans la texture de z obtenue grâce au premier rendu, et considère qu'on est dans l'ombre si le z vu par la lumière est plus petit que le z' du fragment.

    Le picking

    Quand on réalise une application interactive, on peut souhaiter savoir ce qui se trouve sous la souris (objet ou face, voire coordonnée), par exemple pour permettre à l'utilisateur de le désigner (c'est le picking). D'une manière plus générale, on peut souhaiter avoir un retour quant à ce qui en définitive a vraiment été tracé à l'écran (ne serait-ce que pour mieux élaguer les parties non visibles de la scène). De ce point de vue, savoir ce qu'il se passe sous la souris est un cas particulier: il suffit de restreindre la fenêtre à quelques pixels (voire un seul) autour de la souris et se poser la question de ce qui s'affiche dans cette fenêtre. La librairie utilitaire GLU permet de positionner cette fenêtre d'intérêt sans modifier les autres paramètres: il suffit d'ajouter juste avant l'affichage

    Comme le geometric engine réalise tous les changements de repère, la projection à l'écran et le clipping, à la fin de son traitement on peut savoir tout ce qui peut apparaître à l'écran. Le picking consiste donc à interrompre le traitement du rendu à la fin de la phase 3D, juste avant que le raster engine ne génère les fragments (donc rien ne s'affiche, on a fait un tracé virtuel). On demande alors au geometric engine de fournir une liste décrivant ce qu'il y a à tracer, dans le repère de l'écran. Si un face donnée se trouve dans la liste, c'est qu'elle devrait être visible à l'écran (sauf si elle est cachée par un autre objet), et si l'on avait restreint la fenêtre à quelques pixels sous la souris, on en déduit que cette face est juste sous la souris.

    OpenGL prévoit deux modalités pour le picking selon le type d'information que l'on souhaite récupérer: le mode GL_SELECT, qui permet de savoir quel objet ou quelle face est visible, et le mode GL_FEEDBACK, qui permet d'obtenir toutes les informations disponibles sur la scène, coordonnées géométriques dans le repère écran, couleur, coordonnées textures (u,v), etc. On peut par exemple en tirer les coordonnées du point cliqué à la souris à la surface d'un objet. On active ce mode par glRenderMode(mode) (le mode de tracé normal avec affichage correspondant au mode GL_RENDER). Dans les deux cas il faut fournir un buffer où seront stockées les informations retournées par le geometric engine, avec selon le mode glSelectBuffer(size,buff) ou glFeedbackBuffer(size,GL_3D_COLOR_TEXTURE,buff).
    On accède à ce buffer après être sorti du mode picking par
    nbinfo = glRenderMode(GL_RENDER).

  • Dans le mode GL_SELECT, on regroupe les primitives qui constituent une unité logique en insérant des déclarations glPushName(i) entre leurs tracés, ce qui revient à donner un identifiant à ces unité. Le geometric engine inscrit dans le buffer l'identifiant de toute unité dont au moins une primitive se projette dans la zone de l'écran, c'est ainsi que l'on sait quel objet apparaît.

  • Dans le mode GL_FEEDBACK, chaque primitive apparaissant potentiellement à l'écran va générer une entrée dans le buffer, consistant en un identifiant du type de primitive, GL_POLYGON_TOKEN pour un polygone, suivit du nombre de sommets, puis pour chaque sommet, de tous les attributs demandé, soit pour GL_3D_COLOR_TEXTURE les coordonnées x,y,z dans le repère écran, la couleur apparente RGBA, et les coordonnées de texture u,v,s,t (dans le cas général, il y a 4 coordonnées texture). Comme la phase de clipping redécoupe les facettes qui sortent de l'écran, dans le cas d'une fenêtre de 1 pixel de large positionnée sous la souris on obtient ainsi les coordonnées, l'illumination, etc, sur le lieu de la surface situé sous la souris. NB: on peut remonter aux coordonnées dans le repère monde via la fonction gluUnproject(). (Mais cette fonction est coûteuse si l'on veut retrouver plusieurs points, car elle inverse la matrice de transformation à chaque appel. Mieux vaut alors faire soit même l'inversion de matrice pour appliquer la même transformation réciproque à tous les points.)
    A noter que l'on obtient dans ce mode un descriptif de ce qui est visible, mais pas la désignation des objets comme dans le mode GL_SELECT. On peut retrouver cette fonctionnalité en insérant des appels à glPassThrough(f) entre les primitives constituant une unité, dans le même esprit que glPushName(i). On récupère alors dans le buffer l'identifiant de primitive GL_PASS_THROUGH_TOKEN suivit de la valeur f passée en argument, ce qui permet de reconnaître l'unité à laquelle appartiennent les entrées du buffer.

    Lexique

  • graphics engine, moteur graphique: ensemble logiciel et matériel permettant de transformer les ordres graphiques 3D en images à l'écran.
  • geometric engine: portion du graphics engine assurant les transformations géométriques (changements de repère, projection, etc...).
  • culling: élagage (e.g. des faces tournant le dos à l'écran).
  • clipping: élagage des faces qui sortent de l'écran, et découpe des faces partiellement à l'écran.
  • strip / fan: bande, ribambelle / éventail: méthodes de description de maillage qui évitent les redondances (redescription des points qui ont déjà servi pour un autre triangle) en considérant que les triangles s'enchaînent les uns aux autres.
  • raster engine: portion du graphics engine assurant la transformation en pixels (découpe des primitives en lignes puis en fragments).
  • raster / rasteriser: ligne de balayage / tracer ligne par ligne.
  • pixel: point de l'écran.
  • fragment: point (e.g. sur une surface) candidat à apparaître dans le pixel.
  • true color: mode permettant l'affichage `libre' des teintes, sans passer par une table de couleurs, en spécifiant leur décomposition dans les trois composantes rouge, verte et bleue.
  • alpha: opacité (i.e. 1-transparence). Remarque: cette transparence n'est pas colorée, elle ne fait qu'estomper plus ou moins l'arrière-plan.
  • clamper: couper les valeurs sortant de l'intervalle [0,1].
  • blend, blending: mélange (de couleur, entre deux tracés successifs au même endroit).
  • Z-buffer: nom de la structure de donnée, et par extension de l'algorithme, réalisant l'élimination des parties cachées en stockant la valeur de z correspondant à chaque pixel.
  • double buffer: ensemble de deux `écrans' (le front buffer et le back buffer) permettant l'animation sans clignottement: on affiche le premier buffer pendant qu'on dessine dans le second, puis on permute les buffers (swap).
  • Phong, illumination locale: formule décrivant la façon dont une surface renvoit la lumière dans les diverses directions, contribuant à caractériser l'aspect du matériau. Phong est un modèle particulier, isolant les effets de la lumière ambiante, diffuse ou spéculaire (reflets de la source), et prévoyant une couleur spécifique à chacun d'eux.
  • textures: variation des paramètres d'aspect le long de la surface. Dans le cadre restreint d'OpenGL, cela consiste en la donnée d'une image (que l'on désigne usuellement par `texture') et d'une fonction de mapping indiquant comment cette image est plaquée sur la surface. Ceci est obtenu en indiquant pour chaque sommet de la surface ces coordonnées (u,v) dans l'image (i.e. dans l'espace de la texture).
  • picking: action de désigner un objet à la souris.
  • stencil: calque, ou masque, permettant (à l'origine) de n'effectuer des opérations que sur certaines zones de l'écran.
  • masquer: cacher.
  • dithering: mode d'affichage enrichissant la dynamique des couleurs affichables en utilisant un `pattern' (motif rectangulaire) de plusieurs points de couleurs différentes.
  • interpoler: faire passer continument d'une valeur à une autre (e.g. la couleur entre deux (voire trois) sommets, ou la position entre deux instants).
  • enable / disable: activer / désactiver (une option).

    URL références et astuces

    - Les man pages d'OpenGL par ordre alphabétique http://www-evasion.imag.fr/Membres/Fabrice.Neyret/doc/man.html
    - About,Tutoriels, FAQ (site officiel d'OpenGL) http://www.opengl.org/developers/
    - Tutoriel de NeHe sur GameDevelopper http://nehe.gamedev.net/
    - Tutoriel interactif (matrices, materiaux) http://www.xmission.com/~nate/tutors.html
    - Docs OpenGL, GLUT, etc http://www.opengl.org/developers/documentation/specs.html
    - Les extensions d'OpenGL http://oss.sgi.com/projects/ogl-sample/registry/
    - Le cours Siggraph 'advanced opengl' http://www.sgi.com/software/opengl/advanced98/notes/
    - Le pipeline graphique, les librairies EXT http://www.sgi.co.il/support/OGLT/OGLT.html
    - Trucs et astuces (bump, caustics, fresnel, ombres, volumes...) http://toolbox.sgi.com/TasteOfDT/src/exampleCode/WitchesBrew/
    - Trucs et astuces (postscript, halo, ombres, reflets, particules...) http://reality.sgi.com/opengl/tips/
    - Nvidia papers
    - dont l'indispensable Avoiding 19 Common OpenGL Pitfalls
    - Brown university documentation
    - The OpenGL graphics system diagram