Programmation Graphique : CM séance 01
1. Description de l'UE
Présentation :
- UE SINB38AL - Programmation Graphique
- Responsable : Edouard.Thiel@univ-amu.fr
- 6 séances : CM 1h30, TP 3h
Contenu :
- Découverte d'OpenGL (affichage en 3D)
- Introduction aux shaders (programmes sur GPU)
- Affichage de textures (images projetées sur une surface)
- Rendu de lumière (éclairage, lissage)
Intervenant :
- Édouard THIEL : CM, TP
MCCC1 :
- Note finale session unique = max { ET ; 0.7 ET + 0.3 CC }
- À l'examen :
- calculette : non
- document autorisé : un pense-bête manuscrit sur 1 feuille A4 recto-verso
- TP : utilisation d'IAs génératives interdite.
- Présence aux CM et TP obligatoire.
2. Introduction
2.1. Historique
OpenGL = Open Graphics Library
- API multiplateforme de dessins en 2D et 3D,
- lancée par Silicon Graphics en 1992,
- puis standardisée par le consortium Khronos.
- La version courante de OpenGL est 4.6 (2017-).
- Concurrents : Direct3D (de DirectX, 1996-), ...
- Variantes : OpenGL ES (Embedded Systems), WebGL
- Futur : Vulkan (2016-), pour unifier OpenGL et OpenGL ES, et améliorer les performances.
Implémentations :
- matérielle (GPU) : SGI, Nvidia, ATI, Intel, ...
- logicielle ou matérielle : Mesa3D (1995-, libre)
Versions majeures de OpenGL :
- 1.0 (1992) à 1.5 (2003)
- 2.0 (2004) à 2.1 (2006) : introduction des shaders
- 3.0 (2008) à 3.3 (2010) : systématisation des shaders
- 4.0 (2010) à 4.6 (2017-) : dernière version (?)
OpenGL "ancien" : version < 3.2, voir legacy OpenGL
- mode de rendu direct possible, shaders optionnels ;
- certains calculs faits par CPU, puis résultats envoyés au GPU ;
- envoi à chaque frame → beaucoup de bande passante ;
- pipeline graphique fixe → limitation des effets graphiques ;
- déprécié, mais compatibilité ascendante conservée.
OpenGL "moderne" : version 4.x (>= 3.2)
- shaders obligatoires, rendu direct impossible ;
- On envoie données et shaders (programmes) au GPU → exécution massivement parallèle, moins de bande passante, plus d'effets ;
- Plus complexe, sera vu plus tard.
2.2. Philosophie
OpenGL agit comme une machine à états, avec
- des états internes : dessin en cours... ;
- des variables d'état : la couleur courante, les matrices de transformations...
Le principe :
- une variable d'état persiste tant qu'on ne la modifie pas ;
- on fixe les variables d'état avant de créer un élément graphique ;
- les prochains éléments graphiques seront créés avec ces valeurs.
Concrètement :
- OpenGL fait des dessins dans une zone rectangulaire de l'écran, appelée un viewport ;
- Pour dessiner dans le viewport on a besoin d'un contexte OpenGL.
Un contexte OpenGL est un objet opaque qui mémorise tous les états de la machine OpenGL.
2.3. Librairies et Toolkits
Librairies de base, souvent fournies par MESA :
- GL (openGL core) : primitives graphiques ;
- GLU (GL Utility) : graphisme + haut niveau, caméra ;
- GLEW (GL Extension Wrangler) : gestion des extensions.
Préfixes des types et fonctions : gl
, glu
, glew
.
Exemple : glVertex
, gluDisk
, glewInit
.
OpenGL ne permet pas de gérer des fenêtres ni d'intéragir avec le système (clavier, souris, timers, etc). Il faut passer par un GUI toolkit : GLUT, GLFW, GTK, Qt, ...
GLUT : OpenGL Utility Toolkit
- très simple, pour apprendre OpenGL ; licence non libre, n'est plus maintenu (1994-2010) ;
- clone freeglut, très stable (1999-), même API, licence libre ; écrit en C, API compatible C++.
- Inconvénient : la version historique ne permet pas de stocker un
user_data
(par exemplethis
) dans les callbacks ; freeglut a introduit cette possibilité dans la version 3.2 (2019) (voir ce lien section 3.2.2).
GLFW : OpenGL FrameWork
- librairie légère et moderne, multiplateforme ;
- version 1.0 en 2002, 3.4 en 2024 ; licence libre BSD-like ;
- écrite en C, nombreux bindings (C++, Python, Java...) ;
- prend en charge OpenGL, OpenGL ES et Vulkan ;
- Inconvénients : gestion non localisée du clavier ; design de la boucle d'événements.
GTK : the Gimp ToolKit
- librairie complète de composants graphiques (widgets), multiplateforme ;
- versions 1.0 (1998), 2.0 (2002), 3.0 (2011), 4.0 (2020), 4.10 (2023) ; licence libre LGPL ;
- écrite en C, nombreux bindings, dont C++ (GTKmm) ;
- possède un widget
GLArea
pour faire du OpenGL. - Inconvénient : uniquement OpenGL "moderne" (shaders obligatoires).
Qt : the Q toolkit
- librairie complète de composants graphiques (widgets), multiplateforme ;
- démarré en 1991, 3.0 (2001), 4.0 (2005), 5.0 (2012), 6.0 (2020), 6.8 (2024) ;
- double licence libre LGPL et commerciale, développé par "The Qt Company" ;
- écrite en C++, bindings pour d'autres langages (Python) ;
- possède un widget
QOpenGLWidget
pour faire du rendu OpenGL. - Inconvénients : assez lourd à installer, complexe.
En TP, nous allons commencer avec GLFW.
3. Premiers pas en GLFW
3.1. Installation et compilation
Installer g++, GLFW et Mesa sur Ubuntu :
$ sudo apt update
$ sudo apt upgrade
$ sudo apt install g++ make libglfw3-dev libgl1-mesa-dev libglu1-mesa-dev
Compiler : attention aux majuscules et à l'ordre des librairies
$ g++ -Wall -O2 --std=c++17 -c exemple.cpp
$ g++ -o exemple exemple.o -lglfw -lGLU -lGL -lm
$ ./exemple
Il est conseillé d'utiliser ce Makefile :
recopier ce fichier dans le répertoire des fichiers source, puis taper
make all
pour tout compiler.
Windows + WSL
Sous Windows 11+, installer d'abord WSL, puis continuer dans
le terminal Ubuntu : taper les commandes sudo apt ...
ci-dessus.
Si vous avez une version très récente de WSL il est possible que vous ayez ce type d'erreur lors de l'exécution d'un exemple :
$ ./exemple
MESA: error: ZINK: failed to choose pdev
glx: failed to create drisw screen
Dans ce cas, essayez en plaçant cette variable d'environnement :
$ export LIBGL_ALWAYS_SOFTWARE=1
$ ./exemple
Si l'erreur persiste, une solution serait d'installer la toute dernière version de Mesa via un PPA, voir ce lien.
3.2. Hello World
Premier exemple fw01-hello.cpp
:
affichage d'un rectangle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
Commentaires :
- ligne 19 :
glfwInit
initialise GLFW ; - ligne 25 :
glfwCreateWindow
crée une fenêtre et un contexte OpenGL ; - ligne 31 :
glfwMakeContextCurrent
rend courant (actif) le contexte OpenGL de la fenêtre (un seul contexte à la fois peut être courant pour le thread) ; - lignes 33-38 : boucle d'événements (rudimentaire), attente du prochain événement
avec
glfwWaitEvents
; - l'affichage OpenGL est fait dans
displayGL
appelé ligne 35 ; l'appelglfwSwapBuffers
qui suit est obligatoire pour la gestion du double buffer.
Double buffer :
- Technique d'affichage pour éviter le tearing (déchirure), c'est-à-dire des interférences dans l'affichage quand des écritures en mémoire sont faites en même temps.
- Il y a deux buffers qui mémorisent le contenu de la fenêtre : le front buffer (affiché), et le back buffer (caché) dans lequel on fait les nouveaux dessins.
- juste après
displayGL
, donc lorsque les dessins sont achevés dans le back buffer, on échange (swap) les deux buffers, de manière à rendre les nouveaux dessins visibles.
Les dessins OpenGL sont faits dans displayGL
:
8 9 |
|
glclear
vide le buffer des couleurs ;glRectd
dessine un rectangle de coordonnées \((x_1, y_1, x_2, y_2)\) dans le plan \(z=0\).
3.3. Classe MyApp
Exemple fw02-app.cpp
:
réalisation d'une classe MyApp
pour simplifier le développement.
Le squelette de nos programmes prendra cette forme :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
3.4. Reshape window
Exemple fw03-reshape.cpp
:
gestion de la taille de la fenêtre.
On enregistre ligne 22 une callback on_reshape_func
, qui est appelée chaque
fois que la fenêtre change de taille.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Les callbacks pour GLFW étant statiques, on mémorise l'instance ligne 21 avec
glfwSetWindowUserPointer (m_window, this);
ce qui permet de la retrouver ligne 9 en faisant
MyApp* that = static_cast<MyApp*>(glfwGetWindowUserPointer (window));
Le point important pour OpenGL est d'appeler glViewport
ligne 13 avec
les nouvelles dimensions de la fenêtre. Ceci a pour but de mettre à jour
le calcul des coordonnées dans la fenêtre (origine au centre).
3.5. Clavier et rotation
Exemple fw04-clavier.cpp
:
gestion du clavier.
Pour réagir au clavier on enregistre une callback, qui est appelée chaque fois qu'une touche du clavier est enfoncée, répétée ou relâchée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Un des gros défaut de GLFW est de ne proposer qu'une gestion des claviers QWERTY ; on ne peut même pas récupérer le caractère réellement tapé ! On introduit donc une traduction pour le clavier AZERTY :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Pour tester l'effet avec OpenGL on incrémente un angle lorsque la touche espace est enfoncée, et on effectue une rotation du rectangle :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
On reparlera des transformations géométriques plus loin.
3.6. Timer et couleur
Exemple fw05-timer.cpp
:
animation et couleur.
Il est possible de réaliser une animation en plaçant un timeout (ligne 31) puis en mesurant le temps écoulé depuis l'initialisation (ligne 14).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Pour tester avec OpenGL on fait varier la composante rouge de 0 à 1 en 4 secondes,
en appelant glColor3d
ligne 17.
4. Découverte d'OpenGL
Pour aborder OpenGL, nous commençons par la version legacy (sans shaders), nettement plus simple que la version moderne, mais qui présente de nombreux concepts importants. Les shaders seront vus par la suite.
4.1. Coordonnées et sommets
Dans les exemples précédents, on a tracé un rectangle dans le plan \(z = 0\) avec :
glRectd (-0.5, -0.5, 0.5, 0.5);
On distingue 2 espaces :
-
l'espace visible : par défaut c'est le cube \([-1 \ldots 1,\,-1 \ldots 1,\,-1 \ldots 1]\). Tout ce qui est en dehors est coupé.
-
la zone d'affichage dans la fenêtre (le viewport), qui possède une largeur et hauteur en pixels. La fonction
glViewport
permet de mettre à jour la taille.
La mise en correspondance entre l'espace visible et le viewport est automatique, via une projection paramétrable. Par défaut elle ne conserve pas le rapport d'aspect (aspect ratio) largeur/hauteur.
En interne, tout est en 4 dimensions :
- coordonnées \(x, y, z, w\) des sommets ;
- couleurs \(r, g, b, a\) (rouge, vert, bleu, transparence) entre 0 et 1 ;
- matrices de transformations \(4 \times 4\).
La dimension 4 est utilisée pour
- le coupage ;
- les transformations géométriques.
Quand on ne précise pas la coordonnée \(w\) d'un sommet, par défaut \(w = 1\). On en reparle plus tard.
OpenGL fournit un certain nombre de types, utiles pour la portabilité :
GLenum
:unsigned int
GLboolean
:unsigned char
GLbyte
:signed char
(1 octet)GLshort
:short
(2 octets)GLint
:int
(4 octets)GLubyte
:unsigned char
(1 octet)GLushort
:unsigned short
(2 octets)GLuint
:unsigned int
(4 octets)GLfloat
:float
simple précisionGLdouble
:double
double précision
Étant donné que en C les fonctions ne peuvent pas être surchargées, l'API
fournit de nombreuses variantes pour chaque fonction, sous la forme :
gl
fonction[234][u][bsifd][v]
, où
234
: dimension = nombre de coordonnées[u]bsifd
: type coordonnéesv
: adresse d'un tableau de coordonnées
4.2. Dessins
En Opengl legacy, les dessins sont décrits par des sommets :
glVertex2d (x, y);
en 2D (z
= 0,w
= 1)glVertex3d (x, y, z);
en 3 (w
= 1)glVertex4d (x, y, z, w);
en 4D
Voir glVertex
.
La nature du dessin à réaliser à partir de ces sommets est spécifiée en encadrant les sommets par
glBegin(
nature);
glEnd();
où nature est : GL_POINTS
, GL_LINES
, GL_TRIANGLES
, ...
Exemple fw06-dessins.cpp
:
différents types de dessins.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Exemple fw07-dessins.cpp
:
idem, en faisant défiler les différents types de dessin avec la barre espace.
Voir : GlBegin, GlClearColor, GlClear.
Les différents types de dessin sont : (source)
Les polygones sont orientés (flèches oranges) pour distinguer les faces "avant" et "arrière".
Attention : pour GL_POLYGON
le polygone doit être convexe.
4.3. Dégradés de couleurs
Chaque sommet possède des attributs : coordonnées, couleur, etc.
Un sommet est créé dans la couleur courante → il faut la préciser avant de créer le sommet :
glBegin(...);
glColor3d (r, g, b); // entre 0.0 et 1.0
glVertex2d (x, y);
...
glEnd();
Lorsque dans une figure les sommets ont des couleurs différentes, les couleurs sont interpolées pour les pixels internes.
Exemple fw08-couleurs.cpp
:
dégradé de couleurs (taper sur la barre espace pour changer de figure).
4.4. Matrices et modes
En OpenGL legacy, les transformations géométriques sont faites à l'aide de matrices de la façon suivante :
- On appelle
glRotatef
,glTranslatef
,glScalef
, etc. - Ces fonctions modifient la matrice courante.
-
Toutes les primitives graphiques créées ensuite sont transformées par la matrice courante, sommet par sommet en multipliant à droite :
\[ (x, y, z, w)^T \longmapsto M \cdot (x, y, z, w)^T \]
Note : la notation \((x, y, z, w)^T\) signifie qu'on prend la Transposée du vecteur ligne pour avoir un vecteur colonne.
Pour initialiser la matrice courante :
void glLoadIdentity(); // matrice identité
void glLoadMatrixd (const GLdouble* m); // matrice M
Voir glLoadIdentity, glLoadMatrix.
Trois piles de matrices sont utilisées en interne, une pour chacun des modes :
- mode
GL_MODELVIEW
(par défaut) ; - mode
GL_PROJECTION
; - mode
GL_TEXTURE
.
La matrice courante est le sommet de la pile courante.
Pour rendre une pile de matrices courante :
void glMatrixMode (GLenum mode);
On peut empiler et dépiler des matrices avec glPushMatrix
et glPopMatrix
,
voir plus loin.
Quel est le rôle de ces matrices ? Elles sont utilisées par le pipeline graphique :
-
GL_MODELVIEW
: positionnement des objets devant la caméra- modèle : transformations des primitives graphiques
- vue : déplacements devant la caméra
-
GL_PROJECTION
: propriétés de la caméra- type de perspective
- distance focale
- plans de coupe
-
GL_TEXTURE
: placage de textures- positionnement
À quel moment on utilise ces matrices ?
L'idée est d'être toujours en mode GL_MODELVIEW
, sauf lorsqu'on spécifie
la projection, ici dans set_projection
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
On appelle systématiquement set_projection
chaque fois que la fenêtre est
retaillée.
4.5. Projections
Il y a deux types de projection :
La scène est projetée sur le plan de vue, en bleu.
Les dessins sont coupés par des plans de coupe, au nombre de 6 : tout ce qui est au delà est supprimé de l'affichage.
La caméra est par défaut située à l'origine, dirigée vers \(-Oz\),
orientée vers le haut selon l'axe \(Oy\).
La position de la caméra est modifiable par gluLookAt
, voir plus loin.
🔹 Une projection perspective peut être demandée avec :
void glFrustum (GLdouble left, GLdouble right,
GLdouble bottom, GLdouble top,
GLdouble near, GLdouble far);
où near
\(> 0\) et far
\(> 0\).
La partie visible (depuis l'origine) est la pyramide coupée.
Le fait de pouvoir donner indépendamment left
et right
, top
et bottom
permet de découper une scène sur plusieurs fenêtres ou écrans (murs d'images).
Note : la partie d'un solide située entre 2 plans parallèles s'appelle un tronc, en latin frustum.
Voir : glFrustum.
🔹 Il existe une variante symétrique de la projection perspective :
void gluPerspective(GLdouble fovy, GLdouble aspect,
GLdouble near, GLdouble far);
où near
\(> 0\) et far
\(> 0\), aspect
\(=\) w
\(/\) h
, et
fovy
\(= \alpha \in [0 .. 180]\).
Voir : gluPerspective.
🔹 Pour obtenir une projection orthographique on appelle :
void glOrtho(GLdouble left, GLdouble right,
GLdouble bottom, GLdouble top,
GLdouble near, GLdouble far);
où near
, far
\(\in \Bbb{R}\) (le point de vue peut être "dans" la scène).
Voir : glOrtho.
🔹 Il existe enfin une projection orthographique 2D :
void gluOrtho2D(GLdouble left, GLdouble right,
GLdouble bottom, GLdouble top;
L'effet est équivalent à glOrtho(..., -1, 1)
→ les dessins en 2D (\(z = 0\)) sont non coupés.
Par défaut : gluOrtho2D (1, 1, 1, 1);
Voir : gluOrtho2D.
4.6. Placement de la caméra
🔹 Le positionnement de la caméra peut être spécifié par
void gluLookAt(
GLdouble eyeX, GLdouble eyeY, GLdouble eyeZ,
GLdouble cX, GLdouble cY, GLdouble cZ,
GLdouble upX, GLdouble upY, GLdouble upZ);
La caméra sera placée en (eyeX, eyeY, eyeZ)
,
tournée vers le centre (cX, cY, cZ)
de la scène,
en direction du haut (upX, upY, upZ)
.
On doit le faire en mode GL_MODELVIEW
.
Voir : gluLookAt.
🔹 La caméra est habituellement placée de la façon suivante :
La scène est autour de l'origine \(O\),
la caméra regarde vers l'origine,
avec \(0 <\) near
\(<\) cam_z
\(<\) far
.
gluLookAt (0, 0, cam_z, 0, 0, 0, 0, 1, 0);
Exemple fw09-proj.cpp
:
permet de tester les projections et de modifier les paramètres au clavier.
4.7. Rapport d'aspect
On peut maintenant mettre en place la conservation du rapport d'aspect, de manière à ce qu'un carré reste carré lorsque la fenêtre est retaillée.
L'idée est de calculer le rapport d'aspect aspect_ratio
et de l'utiliser
dans la projection :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Exemple fw10-ratio.cpp
:
conservation du rapport d'aspect.
4.8. Transformations géométriques
Des transformations géométriques peuvent être appliquées sur le modèle avec les fonctions suivantes :
glTranslatef (GLfloat x, GLfloat y, GLfloat z);
glRotatef (GLfloat angle, GLfloat x, GLfloat y, GLfloat z);
glScalef (GLfloat x, GLfloat y, GLfloat z);
Voir : glTranslate, glRotate, glScale.
Ces fonctions multiplient la matrice courante par la matrice de transformation correspondante.
Lorsqu'on applique successivement plusieurs transformations, l'ordre des opérations à son importance car la multiplication des matrices est non commutative.
Exemple fw11-ordre.cpp
:
ordre des opérations.
Dans quel ordre faut-il faire les transformations ?
-
Tous les vertex créés sont transformés par la matrice courante \(C\) :
\[ V' \leftarrow C \cdot V \] -
Les fonctions précédentes multiplient la matrice courante \(C\) par la matrice de transformation correspondante \(F\) (à droite) :
\[ C' \leftarrow C \cdot F \] -
Tous les vertex créés ensuite seront transformés par
\[ V' \leftarrow C' \cdot V = C \cdot F \cdot V \] -
Si on appelle une autre transformation \(G\), on obtient
\[ C'' \leftarrow C' \cdot G = C \cdot F \cdot G \] -
Les vertex créés ensuite seront transformés par
\[ V'' \leftarrow C'' \cdot V = C \cdot F \cdot G \cdot V = C \cdot (F \cdot (G \cdot V)) \]
Bilan : la première matrice appliquée à \(V\) est \(G\), puis \(F\) (puis \(C\)).
→ Toujours appeler les fonctions dans l'ordre inverse.
Exemple :
1 2 3 4 5 6 7 8 9 |
|
4.9. Push et pop de matrices
Les fonctions suivantes agissent sur la pile courante :
void glPushMatrix();
duplique la matrice courante au sommet de la pile,
void glPopMatrix();
dépile la pile courante, donc rétablit la matrice sauvegardée par
glPushMatrix
.
Voir glMatrixMode, glPopMatrix.
Exemple fw12-pushpop.cpp
: push et pop de matrices.
Ces fonctions sont donc très utiles lorsqu'on veut réaliser des transformations différentes sur plusieurs figures :
glPushMatrix();
// transformations
// dessin figure 1
glPopMatrix();
glPushMatrix();
// transformations
// dessin figure 2
glPopMatrix();
4.10. Z-buffer
Exemple fw13-depth.cpp
:
activation du Z-buffer (touche b
on/off).
OpenGL utilise un Z-buffer (buffer de profondeur) pour éliminer les parties cachées.
Principe : chaque fois qu'un pixel doit être affiché, on regarde dans le Zbuffer à sa position, quelle est la profondeur (Z) du pixel précédemment affiché.
- si le nouveau pixel est "devant" on l'affiche et on met à jour le Z-buffer,
- sinon on ne l'affiche pas (puisqu'il serait "derrière", donc non visible).
A niveau de GLFW il suffit d'écrire :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
4.11. Antialiasing
L'anti-aliasing, ou anti-crénelage consiste à lisser l'affichage pour éliminer l'effet "escalier" des pixels sur les segments et les bords des triangles.
Le principe :
- on fixe un nombre \(n\) d'échantillons (samples) ;
- chaque pixel au bord du polygone est rendu \(n\) fois avec un léger décalage ;
- une moyenne des couleurs obtenue est calculée pour avoir une transition douce.
Voir : Khronos wiki.
On peut activer l'antialiasing dans GLFW en appelant :
1 2 3 4 5 6 7 |
|
Exemple fw14-samples.cpp
: affichage avec
antialiasing.
-
MCCC = Modalités de contrôle des connaissances et des compétences ; ET = Examen Terminal ; CC = Contrôle Continu. ↩