Aller au contenu

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 exemple this) 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
#include <iostream>
#include <GLFW/glfw3.h>

void displayGL (GLFWwindow* window)
{
    std::cout << __func__ << std::endl;

    glClear (GL_COLOR_BUFFER_BIT);
    glRectd (-0.5, -0.5, 0.5, 0.5);
}

void error_callback (int error, const char* description)
{
    std::cerr << "Error: " << description << std::endl;
}

int main() 
{
    if (!glfwInit()) {
        std::cerr << "GLFW: initialization failed" << std::endl;
        exit (1);
    }
    glfwSetErrorCallback (error_callback);

    GLFWwindow* window = glfwCreateWindow (640, 480, "Hello World!", NULL, NULL);
    if (!window) {
        std::cerr << "GLFW: window creation failed" << std::endl;
        glfwTerminate();
        exit (1);
    }
    glfwMakeContextCurrent (window);

    while (!glfwWindowShouldClose (window))
    {
        displayGL (window);
        glfwSwapBuffers (window);
        glfwWaitEvents();
    }

    glfwDestroyWindow (window);
    glfwTerminate();
}

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'appel glfwSwapBuffers 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 (GL_COLOR_BUFFER_BIT);
    glRectd (-0.5, -0.5, 0.5, 0.5);
  • 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
#include <GLFW/glfw3.h>

class MyApp
{
    bool m_ok = false;
    GLFWwindow* m_window = nullptr;

    void displayGL()
    {
        // dessins OpenGL
    }

public:
    MyApp() {
        // Init GLFW, création de m_window, contexte GL courant
        m_ok = true;
    }

    void run()
    {
        // Boucle d'événements
        while (m_ok && !glfwWindowShouldClose (m_window)) {
            displayGL();
            glfwSwapBuffers (m_window);
            glfwWaitEvents();
        }
    }

    ~MyApp() {
        // destruction de m_window et terminaison de GLFW
    }
}; // MyApp

int main() 
{
    MyApp app;
    app.run();
}

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
class MyApp
{
    ...
    static void on_reshape_func (GLFWwindow* window, int width, int height)
    {
        std::cout << __func__ << " "
            << width << " " << height << std::endl;

        MyApp* that = static_cast<MyApp*>(glfwGetWindowUserPointer (window));
        std::cout << "Test userPointer: "
            << (window == that->m_window ? "ok" : "bad") << std::endl;

        glViewport(0, 0, width, height);
    }

public:

    MyApp()
    {
        ...
        glfwSetWindowUserPointer (m_window, this);
        glfwSetWindowSizeCallback (m_window, on_reshape_func);
        ...
    }
    ...
}; // MyApp

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
class MyApp
{
    ...
    static void on_key_func (GLFWwindow* window, int key, int scancode, 
        int action, int mods)
    {
        std::cout << __func__ << " " << key << " " << scancode << " " 
            << action << " " << mods << std::endl;

        // action = GLFW_PRESS ou GLFW_REPEAT ou GLFW_RELEASE
        if (action == GLFW_RELEASE) return;

        int trans_key = translate_qwerty_to_azerty (key, scancode);
        switch (trans_key) {

        case GLFW_KEY_A : std::cout << "A" << std::endl; break;
        ...
        }
    }

public:

    MyApp()
    {
        ...
        glfwSetKeyCallback (m_window, on_key_func);
        ...
    }
    ...
}; // MyApp

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
    static int translate_qwerty_to_azerty (int key, int scancode)
    {
        // https://www.glfw.org/docs/latest/group__keys.html
        // QWERTY -> AZERTY
        switch (key) {
            case GLFW_KEY_Q : return GLFW_KEY_A;
            case GLFW_KEY_A : return GLFW_KEY_Q;
            case GLFW_KEY_W : return GLFW_KEY_Z;
            case GLFW_KEY_Z : return GLFW_KEY_W;
            case GLFW_KEY_SEMICOLON : return GLFW_KEY_M;
        }
        ...
        return key;
    }

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
class MyApp
{
    ...
    double m_angle = 0;

    void displayGL()
    {
        std::cout << __func__ << std::endl;

        glClear (GL_COLOR_BUFFER_BIT);
        glLoadIdentity();
        glRotated (m_angle, 0, 0, 1);
        glRectd (-0.5, -0.5, 0.5, 0.5);
    }

    ...
    static void on_key_func (GLFWwindow* window, int key, int scancode, 
        int action, int mods)
    {
        ...
        MyApp* that = static_cast<MyApp*>(glfwGetWindowUserPointer (window));
        ...
        switch (trans_key) {

        case GLFW_KEY_SPACE :
            that->m_angle++;
            break;

        }
    }
    ...
}; // MyApp

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
...
#include <cmath>

const double FRAMES_PER_SEC  = 30.0;
const double ANIM_DURATION   = 4.0;

class MyApp
{
    ...
    void displayGL()
    {
        ...
        // change la couleur en fonction du temps
        double time = glfwGetTime();           // durée depuis init
        double slice = time / ANIM_DURATION;
        double r = slice - std::floor(slice);  // partie fractionnaire
        glColor3d (r, 0.5, 0.2);

        glRectd (-0.5, -0.5, 0.5, 0.5);
        ...
    }
    ...
public:
    ...
    void run()
    {
        while (m_ok && !glfwWindowShouldClose (m_window))
        {
            displayGL();
            //glfwWaitEvents();
            glfwWaitEventsTimeout (1.0/FRAMES_PER_SEC);
        }
    }
    ...
}; // MyApp

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écision
  • GLdouble : 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 :
glfonction[234][u][bsifd][v], où

  • 234 : dimension = nombre de coordonnées
  • [u]bsifd : type coordonnées
  • v : 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();

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
    void displayGL()
    {
        //std::cout << __func__ << std::endl;

        glClearColor (0.95, 1.0, 0.8, 1.0);
        glClear(GL_COLOR_BUFFER_BIT);

        glBegin(GL_LINE_STRIP);  // ou GL_POINTS, GL_LINES, etc
        glColor3d (0.9, 0.6, 0.8);
        glVertex2d (-0.4,-0.4);
        glVertex2d ( 0.4,-0.4);
        glVertex2d (-0.4,-0.2);
        glVertex2d ( 0.4, 0.3);
        glVertex2d (-0.5, 0.4);
        glVertex2d ( 0.3, 0.7);
        glEnd();
    }

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)

Primitives graphiques

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
class MyApp
{
    ...
    void displayGL()
    {
        glClear (GL_COLOR_BUFFER_BIT);

        glLoadIdentity();
        glRotated (5, 0, 1.0, 0.15);
        // Dessins
    }

    void set_projection()
    {
        glMatrixMode (GL_PROJECTION);
        glLoadIdentity();

        // glOrtho (...) ou glFrustum (...) ou gluPerspective(...)

        glMatrixMode (GL_MODELVIEW);
    }

    static void on_reshape_func (GLFWwindow* window, int width, int height)
    {
        glViewport (0, 0, width, height);

        MyApp* that = static_cast<MyApp*>(glfwGetWindowUserPointer (window));
        that->set_projection();
    }
    ...

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 :

  • perspective : Projection perspective

  • orthographique : Projection orthographique

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.

  • perspective : Projection perspective

  • orthographique : Projection orthographique

🔹 Une projection perspective peut être demandée avec :

void glFrustum (GLdouble left,   GLdouble right,
                GLdouble bottom, GLdouble top,
                GLdouble near,   GLdouble far);

near\(> 0\) et far \(> 0\).

Légende

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

near\(> 0\) et far \(> 0\), aspect \(=\) w \(/\) h, et fovy \(= \alpha \in [0 .. 180]\).

Légende

Voir : gluPerspective.

🔹 Pour obtenir une projection orthographique on appelle :

void glOrtho(GLdouble left,   GLdouble right, 
             GLdouble bottom, GLdouble top, 
             GLdouble near,   GLdouble far); 

near, far \(\in \Bbb{R}\) (le point de vue peut être "dans" la scène).

Légende

Voir : glOrtho.

🔹 Il existe enfin une projection orthographique 2D :

void gluOrtho2D(GLdouble left,   GLdouble right, 
                GLdouble bottom, GLdouble top; 

Légende

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

Légende

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

Légende

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
class MyApp
{
    double m_aspect_ratio = 1.0;
    ...

    void set_viewport (int width, int height)
    {
        glViewport (0, 0, width, height);
        m_aspect_ratio = (double) width / height;
    }

    void set_projection()
    {
        glMatrixMode (GL_PROJECTION);
        glLoadIdentity();

        double wr = m_cam_hr * m_aspect_ratio;
        glFrustum (-wr, wr, -m_cam_hr, m_cam_hr, m_cam_near, m_cam_far);

        glMatrixMode (GL_MODELVIEW);
    }

    static void on_reshape_func (GLFWwindow* window, int width, int height)
    {
        std::cout << __func__ << " "
            << width << " " << height << std::endl;

        MyApp* that = static_cast<MyApp*>(glfwGetWindowUserPointer (window));
        that->set_viewport (width, height);
        that->set_projection();
    }

    ...

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
glScaled (0.5, 0.5, 0.5);
glRotated (45, 0, 0, 1);
glTranslated (0, -0.5, 0);
glBegin(GL_LINE_LOOP);
glColor3d (0.6, 0.9, 0.5);
glVertex3d (0, 0, 0);
glVertex3d (1, 0, 0);
glVertex3d (1, 1, 0);
glEnd();
fait une translation, puis une rotation, puis un scale.

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
class MyApp
{
    ...
    void displayGL()
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        ...
    }
    ...
public:
    MyApp()
    {
        ...
        glfwMakeContextCurrent (m_window);
        ...
        glEnable(GL_DEPTH_TEST);
        ...
    }
    ...

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
    MyApp()
    {
        ...
        // Hints à spécifier avant la création de la fenêtre
        // https://www.glfw.org/docs/latest/window.html#window_hints_fb
        glfwWindowHint (GLFW_SAMPLES, 16);
        ...

Exemple fw14-samples.cpp : affichage avec antialiasing.


  1. MCCC = Modalités de contrôle des connaissances et des compétences ; ET = Examen Terminal ; CC = Contrôle Continu.