La seconde partie du chapitre est un mini-tutorial pour découvrir OpenGL ; nous indiquons à la fin des pointeurs pour en savoir plus (livres et documentation des fonctions OpenGL sur le web, etc).
OpenGL a été développé pour interfacer les cartes graphiques 3D, mais peut aussi être complètement émulé avec la librairie Mesa (au prix de performances moindres). OpenGL est devenu le standard de fait de l'industrie, et est maintenu par un Consortium de grands groupes informatiques, l'ARB (Architecture Review Board).
Les librairies d'OpenGL sont : libGL (les fonctions de base) et libGLU (des fonctions "Utiles", qui étendent ou simplifient les fonctions de base). Si vous utilisez Mesa, ces librairies risquent de s'appeler libMesaGL et libMesaGLU.
La première chose à faire est de tester si Helium a été configuré avec
OpenGL, en exécutant /chemin-helium/gldemo/gears
.
Si les roues dentées apparaissent, tout va bien ; sinon reportez-vous
au document INSTALL.
Un programme Helium utilisant OpenGL doit comporter la ligne
#include <heliumGL.h>
à la place de
#include <helium.h>
.
Pour compiler un tel programme, il suffit de remplacer --cflags
et
--libs
par --gl-cflags
et --gl-libs
en ligne de commande ou dans
un script. Dans un Makefile, remplacer simplement HE_CFLAGS
et HE_LIBS
par HE_GL_CFLAGS
et HE_GL_LIBS
. À noter, HE_GL_LIBS
contient "-lm
".
Pour compiler un exemple "ex.c
", il suffit donc de taper (avec les
backquotes ` `
) :
gcc ex.c -o ex `/chemin-helium/helium-cfg --gl-cflags --gl-libs`
(remplacer /chemin-helium
par le chemin absolu). Pour faciliter la
compilation des exemples utilisant GLArea, voici un petit
script en sh :
#! /bin/sh
p=/chemin-helium
f=`basename $1 .c`
gcc $f.c -o $f `$p/helium-cfg --gl-cflags --gl-libs`
Appeler ce script "hcompgl
", sauver, taper "chmod +x hcompgl
". Pour
compiler un fichier "ex.c
", on tape "./hcompgl ex.c
" ou "./hcompgl ex
".
Pour compiler un exemple et lancer automatiquement l'exécution si la
compilation a réussie, on tape "./hcompgl ex.c && ex
".
Voici un Makefile simple ; on inclut le fichier de configuration de
Helium /chemin-helium/.config
, où les variables nécessaires sont
déclarées, en particulier $(CC)
, $(HE_GL_CFLAGS)
et $(HE_GL_LIBS)
.
Attention, vérifiez que les lignes décalées commencent bien par un [TAB].
include /chemin-helium/.config
.c.o :
$(CC) -c $(HE_GL_CFLAGS) $*.c
bouton : bouton.o
$(CC) -o $@ $@.o $(HE_GL_LIBS)
Enfin, voici un Makefile sophistiqué, qui affiche un Message d'aide,
permet de compiler plusieurs programmes et de nettoyer le répertoire.
include /chemin-helium/.config
.c.o :
$(CC) -c $(HE_GL_CFLAGS) $*.c
# Rajoutez ici le nom de votre_prog
EXECS = bouton bouton2 bouton3
help ::
@echo "Options du make : help all clean distclean $(EXECS)"
# Rajoutez ici votre_prog : votre_prog.o
bouton : bouton.o
bouton2 : bouton2.o
bouton3 : bouton3.o
all :: $(EXECS)
$(EXECS) :
$(CC) -o $@ $@.o $(HE_GL_LIBS)
clean ::
\rm -f *.o core
distclean :: clean
\rm -f $(EXECS)
La philosophie d'un GLArea est la même que celle d'un Canvas ; il y a une RepaintProc, une ResizeProc et une EventProc. Il y a aussi une InitProc, dont nous allons voir l'utilité.
Pour créer un GLArea on fait
int attr_list[] = { GLX_RGBA, None };
He_node *glarea;
glarea = HeCreateGLArea (frame, attr_list, NULL);
où frame
est le Frame qui hébergera le GLArea. Par défaut, le GLArea
est placé en 0,0 du Frame, et son aspect est un rectangle noir,
sans bord.
La variable attr_list
permet de paramétrer OpenGL, en lui
donnant une liste d'attributs terminés par None
(cf documentation
de glXChooseVisual()
). Ici on demande de coder les couleurs en RGBA
(Red Green Blue Alpha, c'est-à-dire rouge vert bleu transparence).
Le troisième paramètre de HeCreateGLArea, que l'on a mis à NULL
, sert
à partager un contexte avec un autre GLArea déjà créé, par exemple
pour afficher plusieurs vues d'un même objet.
Pour dessiner dans le GLArea, on attache une callback RepaintProc :
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
Le prototype de la callback est
void glarea_repaint_proc (He_node *hn);
où hn
est le GLArea. Chaque fois que le GLArea doit être réaffiché,
Helium appelle la RepaintProc en lui passant le widget GLArea dans
lequel dessiner en OpenGL. N'importe quelle fonction de GL ou de GLU
peut être appelée dans la RepaintProc. Lorsqu'on fait un dessin en OpenGL, il faut d'abord préciser le type de projection (avec ou sans perspective), poser un repère, donner l'éclairage, etc. Ce genre d'initialisation n'a besoin d'être faite qu'une seule fois au début du programme. L'endroit idéal est la callback InitProc, qui n'est appelée qu'une seule fois, juste avant le premier appel de la RepaintProc. Pour attacher une callback InitProc au GLArea on fait :
HeSetGLAreaInitProc (glarea, glarea_init_proc);
Le prototype de la callback est
void glarea_init_proc (He_node *hn);
où hn
est le GLArea. N'importe quelle fonction de GL ou de GLU peut
être appelée dans la InitProc.L'exemple suivant affiche un triangle blanc sur fond noir ; on revient sur les fonctions OpenGL dans la section « 8.7. Dessins en 2D avec OpenGL » :
/* examples/glarea/triangle.c */
#include <heliumGL.h>
He_node *princ, *glarea;
void glarea_init_proc (He_node *hn)
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, 100.0, 0.0, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void glarea_repaint_proc (He_node *hn)
{
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1,1,1);
glBegin(GL_TRIANGLES);
glVertex2f(10,10);
glVertex2f(10,90);
glVertex2f(90,90);
glEnd();
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Triangle");
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetWidth (glarea, 500);
HeSetHeight (glarea, 400);
HeSetGLAreaInitProc (glarea, glarea_init_proc);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
HeFit (princ);
return HeMainLoop (princ);
}
La chose importante à comprendre, est qu'on ne peut pas appeler des fonctions de OpenGL n'importe où dans le programme : il faut le faire dans des endroits prévus pour, ou alors il faut appeler une fonction spéciale, qui demande de "rendre courant un contexte GL". En effet, contrairement aux fonctions de Xlib où on passe de nombreux paramètres, ce qui est un peu lourd à écrire mais plus précis en terme de programmation, les fonctions OpenGL prennent très peu de paramètres. La raison est que OpenGL est une sorte d'automate à états ; on dispose de nombreuses fonctions pour modifier ces états, et le résultat d'une fonction OpenGL dépend de la valeurs des états au moment où on l'appelle. Ces états sont mémorisés dans un contexte GL (un peu comme un GC pour Xlib) mais il n'y a pas de paramètre pour désigner le contexte GL aux fonctions OpenGL ; à la place, il faut rendre courant le contexte GL du GLArea.
Ceci est fait automatiquement par Helium juste avant l'appel de chaque callback InitProc, RepaintProc, ResizeProc et EventProc, si bien que vous pouvez faire directement du OpenGL dans ces callbacks. Dans toute autre fonction qui n'est pas appelée depuis ces callback, comme dans le programme principal, si vous voulez faire du OpenGL, il faut commencer par rendre courant le contexte du GLArea, en faisant
if (HeGLAreaMakeCurrent(hn) < 0) return;
où hn
est le GLArea. La fonction HeGLAreaMakeCurrent renvoie 0
en cas de succès, -1 en cas d'erreur. Si le contexte est déjà courant,
cet appel n'a aucun effet. HeSetGLAreaResizeProc (glarea, glarea_resize_proc);
Le prototype de la callback est
void glarea_resize_proc (He_node *hn, int width, int height);
où hn
est le GLArea, width
et height
sont les nouvelles dimensions.
La différence avec le Canvas est que le GLArea a une Resizeproc par
défaut. Si vous donnez une ResizeProc à votre GLArea, cette
ResizeProc par défaut est annulée. La ResizeProc par défaut est la
fonction suivante (cf glx/glarea.c
) :
void HeGLAreaDefaultResizeProc (He_node *hn, int width, int height)
{
glViewport(0,0, width, height);
}
La fonction glViewport sert à dire dans quelle partie rectangulaire
du GLArea on doit afficher la scène. Les paramètres de glViewport,
exprimés en pixels, sont (x, y, largeur, hauteur) où x,y est la
coordonnée du coin en haut à gauche relativement au coin en haut à
gauche du GLArea. On voit donc que la ResizeProc par défaut demande
que la vue occupe tout le GLArea. Moralité : le glViewport est automatique par défaut, et vous n'avez pas besoin de vous en soucier ni de mettre de ResizeProc (c'était notre idée de départ). Maintenant, si vous avez besoin de faire d'autres choses au moment du Resize, n'oubliez pas d'appeler glViewport dans votre ResizeProc.
Comme pour le Canvas, un HeSetWidth ou un HeSetHeight sur le GLArea déclenche un appel de la ResizeProc puis de la RepaintProc. Un bon endroit où changer la taille du GLArea est la ResizeProc d'un Frame. Un effet amusant lorsqu'un GLArea change de taille est qu'il change aussi les proportions du dessin : dans l'exemple suivant, les triangles affichés s'applatissent lorsqu'on réduit la hauteur de la fenêtre. Il y a une parade pour conserver les proportions, parade que l'on verra plus loin.
/* examples/glarea/resize.c */
#include <heliumGL.h>
He_node *princ, *glarea;
void princ_resize (He_node *hn, int width, int height)
{
HeExpand(glarea, NULL, HE_BOTTOM_RIGHT);
}
void glarea_init_proc (He_node *hn)
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0,100, 0,100);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void glarea_repaint_proc (He_node *hn)
{
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1,0,0);
glBegin(GL_TRIANGLES);
glVertex2f(10,10);
glVertex2f(10,90);
glVertex2f(90,90);
glEnd();
glColor3f(0,0.5,0.75);
glBegin(GL_TRIANGLES);
glVertex2f(20,10);
glVertex2f(50,40);
glVertex2f(90,20);
glEnd();
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Resize et proportions");
HeSetFrameResizeProc (princ, princ_resize);
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetWidth (glarea, 500);
HeSetHeight (glarea, 400);
HeSetGLAreaInitProc (glarea, glarea_init_proc);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
/* ici on laisse la ResizeProc par défaut du GLArea */
HeFit (princ);
return HeMainLoop (princ);
}
HeSetGLAreaEventProc (glarea, glarea_event_proc);
le prototype de la callback est
void glarea_event_proc (He_node* hn, He_event *hev);
où hn
est le GLArea et hev
contient les caractéristiques de
l'évènement. Cette callback est appelée par Helium chaque fois que
l'un des évènements X11 suivants arrive au GLArea :
EnterNotify
: la souris rentre dans le GLArea
LeaveNotify
: la souris sort du GLArea
KeyPress
: une touche est enfoncée
KeyRelease
: une touche est relachée
ButtonPress
: un bouton de la souris est enfoncé
ButtonRelease
: un bouton de la souris est relaché
MotionNotify
: la souris a bougé
Le type He_event
est défini dans include/types.h :
typedef struct He_event_st {
int type, /* Type d'évènement = xev->type */
sx, sy, /* Coords souris / window */
sb; /* Bouton souris filtré : 0,1,2,3 */
Time time; /* Temps en milli secondes */
Window win; /* Le Window X11 du widget */
XEvent *xev; /* Evènement X11 complet */
char str[256]; /* Buffer lu au clavier */
int len; /* Nombre de char dans str */
KeySym sym; /* Symbole de la touche pressée */
} He_event;
Dans l'exemple suivant on affiche tous les évènements :
/* examples/glarea/event.c */
#include <heliumGL.h>
He_node *princ, *glarea;
void glarea_repaint_proc (He_node *hn)
{
printf ("glarea_repaint_proc\n");
}
void glarea_event_proc (He_node *hn, He_event *hev)
{
printf ("glarea_event_proc ");
switch (hev->type) {
case EnterNotify :
printf ("EnterNotify\n");
break;
case LeaveNotify :
printf ("LeaveNotify\n");
break;
case KeyPress :
printf ("KeyPress: \"%s\" keysym = XK_%s len = %d\n",
hev->str, XKeysymToString(hev->sym), hev->len);
break;
case KeyRelease :
printf ("KeyRelease\n");
break;
case ButtonPress :
printf ("ButtonPress bouton %d\n", hev->sb);
break;
case ButtonRelease :
printf ("ButtonRelease bouton %d\n", hev->sb);
break;
case MotionNotify :
printf ("MotionNotify %d,%d\n", hev->sx, hev->sy);
break;
}
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Évènements du glarea");
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
HeSetGLAreaEventProc (glarea, glarea_event_proc);
HeSetWidth (glarea, 300); HeSetHeight (glarea, 300);
HeFit (princ);
return HeMainLoop (princ);
}
Remarques :
hev->sx
et hev->sy
sont des coordonnées souris
par rapport au coin en haut à gauche du GLArea ; ces coordonnées
n'ont rien à voir avec les coordonnées des objets de la scène.
Retrouver les coordonnées d'un sommet dans la scène 3D à partir des
coordonnées souris est une sorte de sport !
HePostRepaint (glarea);
Ceci provoque un appel (à peine) différé de la RepaintProc par Helium.
On peut appeler HePostRepaint depuis n'importe où ; les deux
cas typiques sont l'EventProc du GLArea et la NotifyProc d'un bouton. En fait, HePostRepaint envoie simplement un évènement Expose au Window du GLArea ; cet évènement est traité une fois qu'on est sorti de la callback d'où on a fait l'appel. Lorsque le GLArea reçoit plusieurs Expose très rapprochés, Helium le détecte et ne commande l'appel de la RepaintProc que sur le dernier Expose, pour gagner en fluidité ; donc si vous appelez plusieurs fois de suite HePostRepaint, un seul RepaintProc sera effectué (le dernier du lot).
On peut aussi appeler directement la fonction qui fait office de
RepaintProc pour tout redessiner à un moment donné ; on doit lui
fournir le GLArea, mais ce n'est pas forcément suffisant : il faut
d'abord être sûr que le contexte GL du GLArea est le contexte GL
courant. Examinons les cas de figure : soit my_repaint_proc
la
RepaintProc d'un GLArea gla
. L'appel
my_repaint_proc(gla);
peut être fait depuis toute callback de gla
(puisque Helium rend
courant le contexte GL d'un GLArea avant d'appeler ses callback), par
exemple depuis son EventProc. Mais si vous voulez le faire depuis la
callback d'un bouton par exemple, il faudra écrire
if (HeGLAreaMakeCurrent(gla) == 0) my_repaint_proc(gla);
Moralité : il est fortement conseillé d'adopter la première solution,
avec l'appel de HePostRepaint. On voit donc qu'il y a deux notions, d'un côté les coordonnées des objets et les transformations géométriques tels que rotation, translation, etc ; de l'autre côté, il y a le mode de projection à l'écran, avec perspective (projection cônique) ou sans (projection orthographique).
OpenGL fait cette distinction : il code toutes les transformations et
les projections dans deux matrices : GL_MODELVIEW
et GL_PROJECTION
.
La fonction glMatrixMode()
permet d'indiquer quelle est la matrice
"courante". Toutes les fonctions de transformation, que se soit
géométriques ou de projection, modifient des valeurs de la matrice
courante.
Ainsi la fonction glLoadIdentity()
initialise la matrice courante à
la matrice identité (diagonale à 1 et le reste à 0). Il est fortement
conseillé d'initialiser les deux matrices avant toute chose, et la
place idéale est la callback InitProc.
Revenons au dessin en 2D : pour avoir une vue 2D de notre plan (x,y),
il suffit donc de demander une projection orthographique 2D, et ceci
se fait dans la matrice GL_PROJECTION
. Voici l'InitProc des exemples
précédents "triangle.c
"
et "resize.c
" avec des
commentaires :
void glarea_init_proc (He_node *hn)
{
/* La matrice de projection devient la matrice courante */
glMatrixMode(GL_PROJECTION);
/* On initialise la matrice de projection */
glLoadIdentity();
/* On code la projection orthographique dans la matrice */
gluOrtho2D(0.0, 100.0, 0.0, 100.0);
/* La matrice des transfo. géométriques devient courante */
glMatrixMode(GL_MODELVIEW);
/* On initialise la matrice des transfo. géométriques */
glLoadIdentity();
/* Dans la RepaintProc on sera donc en mode GL_MODELVIEW et
on pourra faire des rotations et translations */
}
Les paramètres de gluOrtho2D()
sont (gauche, droite, bas, haut) : ce
sont les coordonnées de la région visible dans l'espace. Ici on se
retrouve avec 0.0,0.0 en bas à gauche du GLArea et 100.0,100.0 en
haut à droite (quelle que soit la taille en pixels du GLArea).
Passons maintenant aux dessins proprement dits, que l'on a fait dans
la Repaintproc des exemples précédents
"triangle.c
" et
"resize.c
".
On commence par initialiser la couleur du fond avec
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
La commande glClearColor
fixe une couleur RGBA pour le fond, chaque
paramètre étant entre 0.0 et 1.0. Ici on choisit la couleur noire
(0,0,0) et opaque (1). Le dessin du fond proprement dit est fait par
l'ordre glClear(GL_COLOR_BUFFER_BIT)
.
Pour dessiner un objet dans une couleur r,g,b
, il suffit d'appeler avant
la commande
glColor3f(r,g,b);
où r,g,b
sont entre 0.0 et 1.0 . Tous les dessins faits par la suite
auront cette couleur, jusqu'au prochain appel de glColor3f
(c'est le
même principe que XSetForeground pour le Canvas).
Pour dessiner un point de coordonnées réelles x1,y1
on fait
glBegin(GL_POINTS);
glVertex2f(x1,y1);
glEnd();
Pour dessiner une ligne d'un point x1,y1
à un point x2,y2
on fait
glBegin(GL_LINES);
glVertex2f(x1,y1);
glVertex2f(x2,y2);
glEnd();
En fait de nombreux types de dessins peuvent être faits sur le modèle
glBegin(mode);
glVertex2f(x1,y1);
glVertex2f(x2,y2);
....
glVertex2f(xn,yn);
glEnd();
en précisant le mode
et une liste de sommets réels xi,yi
.
Les valeurs possibles de mode
sont :
GL_POINTS
: trace un point pour chacun des n sommets.
GL_LINES
: trace une série de lignes non connectées. Les segments
sont tracés entre le sommet 1 et 2, puis 3 et 4, etc. Si n est
impair, le dernier sommet est ignoré.
GL_LINE_STRIP
: trace une ligne reliant les sommets 1 à 2, puis 2
à 3, jusqu'à n-1 à n.
GL_LINE_LOOP
: identique à GL_LINE_STRIP
, plus une ligne reliant
le sommet n au sommet 1, pour fermer la boucle.
GL_TRIANGLES
: remplis une suite de triangle avec les sommets 1,2,3
puis 4,5,6 etc. Si n n'est pas multiple de 3 les sommets restants
sont ignorés.
GL_TRIANGLE_STRIP
: remplis une suite de triangle avec les sommets
1,2,3 puis 3,2,4 puis 3,4,5 puis 5,4,6 etc. L'ordre des sommet
est pris pour respecter l'orientation des triangles.
GL_TRIANGLE_FAN
: remplis une suite de triangle avec les sommets
1,2,3 puis 1,3,4 puis 1,4,5 etc. Tous les triangles ont le
sommet 1 en commun.
GL_QUADS
: remplis une suite de quadrilatères de sommets 1,2,3,4
puis 5,6,7,8 etc. Si n n'est pas multiple de 4 les sommets
restants sont ignorés.
GL_QUADS
: remplis une suite de quadrilatères de sommets 1,2,4,3
puis 3,4,6,5 puis 5,6,8,7 etc. L'ordre des sommet est pris pour
respecter l'orientation des quadrilatères.
GL_POLYGON
: remplis un polygone avec les n sommets. Si n est
inférieur à 3 rien n'est tracé. Le polygone ne doit pas se croiser
et il doit être convexe, sinon le résultat est imprévisible.
glPointSize(size);
où size
est un réel, indiquant la taille du carré qui représentera le
point à l'écran, en nombre de pixels. La taille par défaut est 1. Si
l'anti-aliasing est actif, l'affichage donnera un disque (c'est là que
la valeur réelle de size
prend un sens). Pour changer l'épaisseur
de tracé d'une ligne, on fait
glLineWidth(width);
où width
est un réel représentant l'épaisseur en nombre de pixels, en
coupe verticale si la pente est inférieure à 1, sinon en coupe
horizontale. Pour activer ou désactiver l'anti-aliasing on fait
glEnable(GL_LINE_SMOOTH);
glDisable(GL_LINE_SMOOTH);
Par défaut l'anti-aliasing est désactivé, ce qui économise du temps
de calcul. glVertex3f()
. Par exemple pour tracer un point de coordonnées
réelles x1,y1,z1
on fait
glBegin(GL_POINTS);
glVertex3f(x1,y1,z1);
glEnd();
Ce qui change par rapport au 2D, c'est le mode de projection,
nécessaire pour visualiser une scène 3D sur un écran 2D. OpenGL
propose différents modes de projections, la plus réaliste étant la
projection en perspective. À la place de gluOrtho2D
on appelle
gluPerspective(fovy, aspect, near, far);
où fovy
est l'angle de vue, entre 0.0 et 180.0 degrés ; aspect
est
le ratio largeur/hauteur ; near
et far
sont les distances des plans
de coupe à la camera (toujours positives).
L'angle de vue fovy
donne le même effet que le zoom d'un appareil
photo : un petit angle rapproche les objets comme un téléobjectif,
alors qu'un grand angle donne une vue panoramique de la scène.
Le paramètre aspect
permet de fixer les proportions entre largeur et
hauteur de la vue. Si on met toujours 1.0, on aura une déformation si
l'on redimensionne le GLArea. Pour au contraire conserver les
proportions des objets dans la vue, il suffit de donner comme valeur
: largeur du GLArea / hauteur du GLArea.
Un plan de coupe permet d'économiser beaucoup de temps de calcul, en
annulant tous les dessins qui sont de l'autre côté du plan de coupe.
Dans le mode de projection perspective, la partie visible est un
volume limité par 6 plans de coupes : deux plans perpendiculaires à
la direction du point de vue, distants de near
et de far
du point de
vue ; tout ce qui est devant le plan near
ou derriêre le plan far
est
coupé. Les 4 autres plans partent du point de vue (comme le sommet
d'une pyramide) et matérialisent l'angle de vue ; ils correspondent en
projection aux côtés du GLArea.
Par défaut, la caméra est en 0,0,0 et regarde -Oz. Si les objets de la scène ont des coordonnées dans un "voisinage de l'origine", on peut demander de reculer toute la scène de quelques unités dans l'axe des z, avec
glTranslated (0, 0, -dist);
où dist
est une valeur positive, par exemple 10. On obtient ainsi le
même effet qu'en reculant l'appareil photo. Attention, lorsqu'on fait
une translation, à garder le point de vue dirigé vers la scène, sinon
on ne voit plus rien ! Dans l'exemple suivant on affiche une vue en perspective de 2 triangles en couleurs dans l'espace :
/* examples/glarea/perspective.c */
#include <heliumGL.h>
He_node *princ, *panel, *glarea;
void princ_resize (He_node *hn, int width, int height)
{
HeExpand (panel, NULL, HE_RIGHT);
HeExpand (glarea, NULL, HE_BOTTOM_RIGHT);
}
void butt_proc (He_node *hn)
{
HeQuit(0);
}
void glarea_repaint_proc (He_node *hn)
{
/* Mode perspective :
* au lieu de le faire une seule fois dans l'InitProc, on le
* fait à chaque fois : ça permet de conserver les proportions
* de perspective. On aurait pu le faire dans la ResizeProc ...
*/
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60, (GLdouble) HeGetWidth(hn)/HeGetHeight(hn),
1, 100);
/* Coordonnées de la scène :
* on recule tout ce qui va être dessiné ; de la sorte on peut
* travailler avec des z autour de 0, et pas uniquement < 0.
*/
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslated (0, 0, -4);
/* Init du fond : noir opaque */
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(1,0,0); /* Rouge */
glBegin(GL_TRIANGLES);
glVertex3f(-1, -1, 0.5);
glVertex3f(0, 1, 0.5);
glVertex3f(1, -1, 0.5);
glEnd();
glColor3f(0,1,0); /* Vert */
glBegin(GL_TRIANGLES);
glVertex3f(1, 1, -0.5);
glVertex3f(-1, 0.3, 1.5);
glVertex3f(0.8, -2, -0.5);
glEnd();
/* Le triangle vert est censé traverser le triangle rouge ;
* or ici il apparait au dessus du rouge, car il est dessiné en
* second. Il faut donc activer le Zbuffer.
*/
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Perspective");
HeSetFrameResizeProc (princ, princ_resize);
panel = HeCreatePanel (princ);
HeCreateButtonP (panel, "Quit", butt_proc, NULL);
HeFit(panel);
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetY (glarea, HeGetHeight(panel)+2);
HeSetWidth (glarea, 500);
HeSetHeight (glarea, 400);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
HeFit (princ);
return HeMainLoop (princ);
}
Dans cet exemple on constate que les proportions sont bien conservées lorsqu'on redimensionne la fenêtre. Par contre, le triangle vert apparaît "au dessus" du rouge, alors qu'ils sont sensés se traverser d'après leur coordonnées. Il faut donc activer le Zbuffer d'Opengl, c'est-à-dire le calcul des parties cachées depuis le point de vue. Il y a trois choses à faire dans différents endroits du programme pour activer le Zbuffer :
int attr_list[] = { ..., GLX_DEPTH_SIZE, 1, ... };
glEnable(GL_DEPTH_TEST);
glClear(... | GL_DEPTH_BUFFER_BIT);
Après corrections, l'exemple "perspective.c
" devient "zbuffer.c
" :
/* examples/glarea/zbuffer.c */
#include <heliumGL.h>
He_node *princ, *panel, *glarea;
void princ_resize (He_node *hn, int width, int height)
{
HeExpand (panel, NULL, HE_RIGHT);
HeExpand (glarea, NULL, HE_BOTTOM_RIGHT);
}
void butt_proc (He_node *hn)
{
HeQuit(0);
}
void glarea_init_proc (He_node *hn)
{
glEnable(GL_DEPTH_TEST);
}
void glarea_repaint_proc (He_node *hn)
{
/* Mode perspective : au lieu de le faire une seule fois dans
* l'InitProc, on le fait à chaque fois : ça permet de conserver
* les proportions de perspective.
*/
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60, (GLdouble) HeGetWidth(hn)/HeGetHeight(hn),
1, 100);
/* Coordonnées de la scène :
* on recule tout ce qui va être dessiné ; de la sorte on peut
* travailler avec des z autour de 0, et pas uniquement < 0.
*/
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslated (0, 0, -4);
/* Init du fond (noir opaque) et du Zbuffer */
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(1,0,0); /* Rouge */
glBegin(GL_TRIANGLES);
glVertex3f(-1, -1, 0.5);
glVertex3f(0, 1, 0.5);
glVertex3f(1, -1, 0.5);
glEnd();
glColor3f(0,1,0); /* Vert */
glBegin(GL_TRIANGLES);
glVertex3f(1, 1, -0.5);
glVertex3f(-1, 0.3, 1.5);
glVertex3f(0.8, -2, -0.5);
glEnd();
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, /* Codage couleurs */
GLX_DOUBLEBUFFER, /* Double buffer */
GLX_DEPTH_SIZE, 1, /* Zbuffer */
None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Perspective et Zbuffer");
HeSetFrameResizeProc (princ, princ_resize);
panel = HeCreatePanel (princ);
HeCreateButtonP (panel, "Quit", butt_proc, NULL);
HeFit(panel);
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetY (glarea, HeGetHeight(panel)+2);
HeSetWidth (glarea, 500);
HeSetHeight (glarea, 400);
HeSetGLAreaInitProc (glarea, glarea_init_proc);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
HeFit (princ);
return HeMainLoop (princ);
}
On en a profité pour initialiser OpenGL avec le double buffer dans
attr_list
. Le but est d'éviter l'affichage de vues incomplètement
dessinées. Pour ce faire, OpenGL conserve deux images en mémoire,
celle qui est affichée et celle dans laquelle on dessine la future
image ; Helium permute automatiquement les deux images à la fin de
chaque RepaintProc (en appelant HeGLAreaSwapBuffers(glarea);
).
Comparez la stabilité de l'affichage en glissant une petite fenêtre
ou un icone devant le GLArea de "perspective.c
" et "zbuffer.c
".
Tout le GLArea sert pour simuler le trackball : les mouvements verticaux et horizontaux passant par le centre du GLArea font les rotations correspondantes, et un mouvement circulaire autour du centre du GLArea fait la rotation sur le troisième axe. En ce qui concerne les zooms, seul le déplacement vertical est considéré : on se rapproche vers le haut et on s'éloigne vers le bas.
Dans l'exemple suivant, le bouton 1 de la souris sert pour le trackball, le bouton 2 sert pour changer l'angle de vue et le bouton 3 sert pour éloigner le point de vue. Les fonctions sont expliquées après l'exemple.
/* examples/glarea/demotb.c */
#include <heliumGL.h>
He_node *princ, *panel, *glarea;
He_trackball info_tb;
void princ_resize (He_node *hn, int width, int height)
{
HeExpand (panel, NULL, HE_RIGHT);
HeExpand (glarea, NULL, HE_BOTTOM_RIGHT);
}
void butt_proc (He_node *hn)
{
HeQuit(0);
}
void glarea_init_proc (He_node *hn)
{
HeTbInitRotations(&info_tb);
HeTbInitFovy(&info_tb, 60, 5, 120);
HeTbInitZdist(&info_tb, 4, 1, 10);
glEnable(GL_DEPTH_TEST);
}
void glarea_repaint_proc (He_node *hn)
{
/* Mode perspective : au lieu de le faire une seule fois dans
* l'InitProc, on le fait à chaque fois : ça permet de conserver
* les proportions de perspective.
*/
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective( HeTbGetFovy(&info_tb),
(GLdouble) HeGetWidth(hn)/HeGetHeight(hn),
1, 100 );
/* Coordonnées de la scène :
* on recule tout ce qui va être dessiné ; de la sorte on peut
* travailler avec des z autour de 0, et pas uniquement < 0.
*/
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslated (0, 0, -HeTbGetZdist(&info_tb));
/* On tourne le repère d'après le trackball */
HeTbMultRotations(&info_tb);
/* Init du fond (noir opaque) et du Zbuffer */
glClearColor(0,0,0,1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(1,0,0); /* Rouge */
glBegin(GL_TRIANGLES);
glVertex3f(-1, -1, 0.5);
glVertex3f(0, 1, 0.5);
glVertex3f(1, -1, 0.5);
glEnd();
glColor3f(0,1,0); /* Vert */
glBegin(GL_TRIANGLES);
glVertex3f(1, 1, -0.5);
glVertex3f(-1, 0.3, 1.5);
glVertex3f(0.8, -2, -0.5);
glEnd();
}
void glarea_event_proc (He_node *hn, He_event *hev)
{
switch (hev->type) {
case ButtonPress :
HeTbMemoPointer (&info_tb, hev);
break;
case MotionNotify :
switch (hev->sb) {
case 1 : HeTbEventRotations(&info_tb, hn, hev);
HePostRepaint(hn);
break;
case 2 : HeTbEventFovy (&info_tb, hn, hev);
HePostRepaint(hn);
break;
case 3 : HeTbEventZdist (&info_tb, hn, hev);
HePostRepaint(hn);
break;
}
break;
}
}
int main (int argc, char *argv[])
{
int attr_list[] = { GLX_RGBA, /* Codage couleurs */
GLX_DOUBLEBUFFER, /* Double buffer */
GLX_DEPTH_SIZE, 1, /* Zbuffer */
None };
HeInit (&argc, &argv);
princ = HeCreateFrame();
HeSetFrameLabel (princ, "Demo du trackball");
HeSetFrameResizeProc (princ, princ_resize);
panel = HeCreatePanel (princ);
HeCreateButtonP (panel, "Quit", butt_proc, NULL);
HeFit(panel);
glarea = HeCreateGLArea (princ, attr_list, NULL);
HeSetY (glarea, HeGetHeight(panel)+2);
HeSetWidth (glarea, 500);
HeSetHeight (glarea, 400);
HeSetGLAreaInitProc (glarea, glarea_init_proc);
HeSetGLAreaRepaintProc (glarea, glarea_repaint_proc);
HeSetGLAreaEventProc (glarea, glarea_event_proc);
HeFit (princ);
return HeMainLoop (princ);
}
Voici des explication sur ce qui est fait dans cet exemple. Pour
utiliser les fonctions du trackball on déclare une variable info_tb
de type He_trackball
que l'on passe par adresse à toutes les
fonctions. L'initialisation du trackball est faite dans l'InitProc,
avec les lignes
HeTbInitRotations(&info_tb);
HeTbInitFovy(&info_tb, 60, 5, 120);
HeTbInitZdist(&info_tb, 4, 1, 10);
La première ligne initialise les angles de rotation ; la seconde
ligne initialise l'angle de vue à 60 degrés, et fixe l'intervalle de
5 à 120 pour cet angle ; la troisième ligne initialise la distance
de l'observateur à la scène à 4, et fixe l'intervalle de distance de
1 à 10 (l'ordre de grandeur de ces distances est relatif à l'ordre de
grandeur des coordonnées de la scène dans l'espace). La EventProc permet de traduire les mouvements de la souris en mouvements du trackball :
switch (hev->type) {
case ButtonPress :
HeTbMemoPointer (&info_tb, hev);
break;
case MotionNotify :
switch (hev->sb) {
case 1 : HeTbEventRotations(&info_tb, hn, hev);
HePostRepaint(hn);
break;
case 2 : HeTbEventFovy (&info_tb, hn, hev);
HePostRepaint(hn);
break;
case 3 : HeTbEventZdist (&info_tb, hn, hev);
HePostRepaint(hn);
break;
}
break;
}
Lors du ButtonPress on mémorise le point de départ du clic souris.
Dans le MotionNotify on assigne un bouton à chaque fonctionnalité du
trackball (on peut donc changer cette assignation, ou les utiliser à
d'autres fins). Chaque appel à HeTbEvent*
est suivi par un appel à
HePostRepaint
pour mettre à jour l'affichage avec la nouvelle
vue. Les transformations sont toutes faites dans la RepaintProc :
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective( HeTbGetFovy(&info_tb),
(GLdouble) HeGetWidth(hn)/HeGetHeight(hn),
1, 100 );
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslated (0, 0, -HeTbGetZdist(&info_tb));
HeTbMultRotations(&info_tb);
On utilise HeTbGetFovy
pour connaître l'angle de vue, HeTbGetZdist
pour avoir la distance d'éloignement, puis HeTbMultRotations
pour
appliquer les rotations. Attention, ces transformations doivent se
faire exactement dans cet ordre. gldemo/nurbs.c
permet de tester les différents modes
d'échantillonnage des nurbs. Le source est commenté. Cette page web à l'EPFL contient de nombreux pointeurs sur des documents pour approfondir OpenGL ; on y trouve en particulier les ouvrages de référence de Addison-Wesley en ligne : le guide de programmation, et le manuel de référence (en anglais).
On trouve bien entendu une mine d'information chez SGI et sur opengl.org .