Index   Table   Précédent   Suivant

8. Dessins en OpenGL

Dans ce chapitre nous présentons le widget GLArea, qui sert à faire des dessins en 2D ou 3D avec OpenGL, et à réagir au clavier et à la souris.

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).

Index   Table   Précédent   Suivant

8.1. Courte présentation de OpenGL

OpenGL est un langage pour le développement d'applications graphiques, produisant des images en couleurs d'objets 3D. Les fonctions de OpenGL permettent de construire des modèles géométriques, de représenter intéractivement des scènes dans l'espace, de contrôler les propriétés de couleurs, de matériaux et d'éclairage, de manipuler les pixels, et donne accès à la transparence, l'antialiasing, le plaquage de textures et à des effets atmosphériques.

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).

Index   Table   Précédent   Suivant

8.2. Installation et compilation

Le toolkit Helium est réparti en deux librairies : libHelium, qui contient toutes les fonctions ne dépendant que de Xlib, et libHeliumGL, qui contient les fonctions pour interfacer OpenGL. Nous avons fait cette séparation pour que les applications sous Helium qui n'utilisent pas OpenGL puissent être compilées indépendament.

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)
Index   Table   Précédent   Suivant

8.3. Le widget GLArea

Le widget GLArea est un window X11 dans lequel on peut faire du OpenGL, via Mesa ou le GL natif de la carte graphique (sur SGI par exemple). Helium utilise l'extension GLX pour interfacer X11 et OpenGL.

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);
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);
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);
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
    /* 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;
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.

Index   Table   Précédent   Suivant

8.4. Redimensionner le GLArea

La ResizeProc du GLArea fonctionne exactement comme la ResizeProc du Canvas : chaque fois que la taille du GLArea change, la callback ResizeProc du GLArea est automatiquement appelée (si définie), juste avant que la RepaintProc ne soit appelée à son tour. Au premier affichage du GLArea, les callbacks du GLArea sont appelées dans cet ordre : l'InitProc, puis la ResizeProc, puis la RepaintProc. Pour attacher une callback ResizeProc au GLArea on fait
    HeSetGLAreaResizeProc (glarea, glarea_resize_proc);
Le prototype de la callback est
    void glarea_resize_proc (He_node *hn, int width, int height);
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
    /* 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);
    }

Index   Table   Précédent   Suivant

8.5. Évènements dans un GLArea

La EventProc du GLArea fonctionne exactement comme la EventProc du Canvas : pour recevoir des évènements dans un GLArea, il suffit d'attacher une callback EventProc avec
    HeSetGLAreaEventProc (glarea, glarea_event_proc);
le prototype de la callback est
    void glarea_event_proc (He_node* hn, He_event *hev);
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 :

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
    /* 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 :

Index   Table   Précédent   Suivant

8.6. Provoquer un réaffichage

Comme pour le Canvas, on peut provoquer un réaffichage du GLArea avec
    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.

Index   Table   Précédent   Suivant

8.7. Dessins en 2D avec OpenGL

OpenGL représente tous les objets dans l'espace (x,y,z). Pour faire des dessins en 2D, il suffit de donner des coordonnées avec z=0 d'une part, et d'autre part de regarder le plan (x,y) en s'éloignant un peu sur l'axe des z.

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);
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 : On peut changer la taille d'un point, l'épaisseur des lignes, et même activer l'antialiasing, c'est-à-dire "lisser" l'affichage en enlevant les "marches d'escalier". Pour changer la taille d'un point on fait
    glPointSize(size);
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);
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.

Index   Table   Précédent   Suivant

8.8. Dessins en 3D avec OpenGL

C'est en 3D que OpenGL montre sa vraie puissance. Tout ce qu'on a dit pour les dessins en 2D reste valable pour les dessins en 3D. On doit simplement donner les 3 coordonnées de chaque sommet avec la fonction 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);
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);
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
    /* 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
    /* 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".

Index   Table   Précédent   Suivant

8.9. Simuler un trackball

Nous avons inclus dans Helium des fonctions pour changer intéractivement de vue sur la scène 3D. Ces fonctions sont très faciles d'emploi ; elles permettent de faire des rotations sur les 3 axes (à la façon d'un trackball), de changer l'angle de vue (à la façon d'un téléobjectif) ou encore de déplacer le point de vue (en se rapprochant ou en s'éloignant).

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
    /* 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.

Index   Table   Précédent   Suivant

8.10. Exemples

Le programme gldemo/nurbs.c permet de tester les différents modes d'échantillonnage des nurbs. Le source est commenté.

Index   Table   Précédent   Suivant

8.11. En savoir plus sur OpenGL

Le guide officiel pour apprendre OpenGL est en anglais : "OpenGL Programming Guide", chez Addison Wesley.

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 .


Index   Table   Début   Suivant