Aller au contenu

Programmation Graphique : CM séance 02

5. OpenGL moderne

5.1. Introduction

OpenGL peut être décrit sous la forme d'un client-serveur :

  • le client est l'application, qui tourne sur le CPU ;
  • le serveur est la carte graphique, animée par le GPU, qui gère sa propre mémoire.

Le client et le serveur dialoguent via le bus de la carte graphique. Même s'il peut avoir une bande passante énorme (>100 Go/s sur les bus PCIE récents), il peut constituer un goulot d'étranglement, c'est pourquoi il convient de limiter le volume des dialogues, par exemple en stockant le plus possible les données dans le serveur.

L'idée générale est que le client envoie les données et traitements à effectuer au serveur, qui les stocke, puis le client "pilote" les opérations et les dessins à effectuer par le serveur.

Pour augmenter les possibilités de traitement, OpenGL 2.0 a adopté l'usage des shaders :

  • petits programmes, compilés par le driver, puis exécutés par le GPU ;
  • écrits dans le langage GLSL (GL Shader Langage, inspiré du C).

OpenGL "moderne", versions 3.2+ :

  • abandon du mode immédiat de rendu avec glBegin()...glEnd() ;
  • utilisation systématique des shaders ;
  • dépréciation des fonctions :

    • glBegin, glEnd, glVertex, glColor (mode immédiat),
    • les primitives GL_QUADS et GL_POLYGON,
    • glMatrixMode, glPushMatrix, glPopMatrix,
    • glFrustum, glPerspective, glOrtho, glOrtho2D,
    • glLoadIdentity, glRotatef, glTranslatef, glScalef,
    • gluLookAt.

Ces fonctions sont remplacées par des manipulations explicites de structures de données et de matrices, ce qui est plus verbeux mais offre plus de possibilités.

On peut toutefois choisir un niveau de compatibilité :

  • le core profile = sans les fonctions dépréciées ;
  • le compatibility profile = core + fonctions dépréciées.

5.2. Pipeline de rendu

OpenGL implémente un pipeline de rendu, c'est-à-dire une succession séquentielle d'étapes depuis les données initiales (sommets, couleurs, etc) jusqu'à l'image finale.

Ce pipeline de rendu a considérablement évolué depuis les premières versions ; depuis la version 4.0 il est constitué des étapes suivantes :

block-beta
    columns 5
    A space B space C
    space:5
    F space E space D
    space:5
    G space H space I
    space:5
    space:2 K space I

    A["Vertex Data"] --> B
    B("Vertex\nShader") --> C
    C("Tessellation\nControl Shader") --> D
    D("Tessellation\nEvaluation Shader") --> E
    E("Geometry\nShader") --> F
    F("Primitive\nSetup") --> G
    G("Culling and\nClipping") --> H
    H("Rasterization") --> I
    I("Fragment\nShader") --> J
    J("Per-Fragment\nOperations") --> K("Final Image")

    style A fill:palegreen
    style B fill:lightpink 
    style C fill:lavender
    style D fill:lavender
    style E fill:lavender
    style I fill:lightpink
    style K fill:lightcyan

Chaque étape est effectuée en parallèle, et distribuée sur les nombreux cœurs du GPU.

Il y a 5 types de shaders, dont 2 sont obligatoires : le Vertex Shader et le Fragment Shader.

Examinons les étapes :

  1. Vertex Data

    • Les données sont envoyées au serveur :

      • le profile compatibility permet d'envoyer les données au moment du dessin ;
      • le profile core impose que toutes les données soient stockées dans des buffers objects, qui sont des zones de la mémoire du serveur.
    • Le client demande de dessiner des primitives géométriques, au moyen des commandes de dessin d'OpenGL, par exemple glDrawArrays. Il faut au préalable dire quelles données on va utiliser, et comment elles sont organisées en mémoire.

  2. Vertex Shader (OpenGL 2.0)

    • Pour chaque sommet utilisé par une commande de dessin, un Vertex Shader va être appelé, pour traiter les données liées à ce sommet (coordonnées, couleur, normale, texture...).
    • Seules les données concernant ce sommet lui sont présentées (le shader n'a pas accès aux autres sommets ou données).
    • un shader simple peut transmettre les données telles quelles (pass-through shader), un shader plus évolué peut effectuer des transformations géométriques, des calculs de couleur, etc.
    • une application peut définir de multiples Vertex Shaders, mais un seul peut être actif à la fois.
    • Si un sommet est utilisé dans plusieurs dessins, une implémentation peut supprimer les appels multiples en mémorisant le résultat du shader dans un cache.
  3. Tessellation Shaders (OpenGL 4.0)

    • S'ils sont activés, le Tessellation Control Shader reçoit les données en sortie du Vertex Shader puis les envoie au Tessellation Evaluation Shader.
    • La tessellation consiste à subdiviser une maille (mesh), par exemple une triangulation, en une maille plus fine, pour améliorer le rendu. Ce calcul peut prendre en compte par exemple la distance à la caméra pour ajuster le niveau de détail.
    • Les sommets sont organisés en patch ; les shaders sont appelés pour chaque sommet du patch. Chaque shader a accès aux données de tous les sommets du patch en entrée, mais aussi en sortie (données partagées).
  4. Geometry Shader (OpenGL 3.2)

    • S'il est activé, traite les primitives géométriques. Par exemple un triangle strip est vu comme une suite de triangles.
    • Il reçoit une primitive géométrique à la fois, puis en renvoie zéro, une ou plusieurs. Il peut subdiviser une primitive, interpoler des sommets, modifier la nature d'une primitive (transformer un segment en point, un point en segment), etc.
    • Il peut accéder aux primitives ou aux sommets adjacents.
  5. Primitive Setup

    • Cette étape réorganise les primitives géométriques, dont les sommets sont issus des étapes précédentes.
  6. Culling and Clipping

    • Le culling consiste à supprimer certains triangles selon leur face apparente.
    • Le clipping consiste à couper les primitives dont une partie sort du viewport, de manière à ce que tous les sommets soient à l'intérieur du viewport.
  7. Rasterization

    • Le rasterizer découpe chaque primitive en fragments.
    • Un fragment est un "pixel candidat", c'est-à-dire qu'il pourra encore être supprimé avant de devenir un pixel dans le framebuffer.
    • Chaque fragment reçoit comme paramètres le résultat de l'interpolation des paramètres des sommets (position, couleur, normale, etc).
  8. Fragment Shader (OpenGL 2.0)

    • Ce shader est obligatoire. C'est la dernière étape où on a un contrôle direct sur la couleur d'un pixel.
    • Le Fragment Shader est appelé pour chaque fragment, et reçoit les paramètres interpolés (position, couleur, normale, etc).
    • Il peut transmettre la couleur telle quelle (pass-through), ou faire des calculs plus ou moins élaborés, par exemple d'éclairage, de transparence, de texture, etc.
    • Il n'a pas accès aux valeurs des autres fragments.
  9. Per-Fragment Operations

    • Cette étape est un post-traitement, appliqué aux résultats du Fragment Shader, pour déterminer la couleur finale des pixels dans le framebuffer.
    • La visibilité des fragments est déterminée par rapport à divers élements :
      • le pixel appartient à la fenêtre (il n'est pas recouvert par une autre fenêtre) ;
      • le Z-buffer, si activé ;
      • le tampon pochoir (stencil buffer), si activé.
    • Des opérations sont réalisées, telles que : la transparence, le mélange (blending), des opérations logiques, etc.

Remarque : il existe aussi un Compute Shader (OpenGL 4.3) qui permet de tirer partie de la puissance du GPU pour effectuer des calculs non graphiques.

5.3. Chargeur OpenGL

Les implémentations d'OpenGL peuvent varier fortement (fabriquant, version, modèle de carte, driver...) ; OpenGL n'est en fait qu'une spécification.

Pour utiliser le core profile, il faut d'abord interroger OpenGL pour obtenir la liste des fonctions disponibles et un pointeur vers chaque fonction.

Ce travail est lourd (il y a des centaines de fonctions) et il est donc délégué à un chargeur, qui ne fait pas partie d'OpenGL à proprement parler.

Il existe plusieurs chargeurs OpenGL, et qui sont tenus régulièrement à jour :

  • GLAD, un générateur de chargeur d'extension pour OpenGL, Vulkan, etc ;
  • GL3W, spécialisé pour le core profile d'OpenGL 3 et 4 ;
  • glbinding, un nouveau chargeur en C++ ;
  • etc.

Dans ce cours nous utiliserons GLAD. Il génère un chargeur via un formulaire en ligne, qui permet de configurer selon ses besoins, puis générer des fichiers glad.c, glad.h et khrplatform.h (environ 25000 lignes !) à placer directement dans le répertoire du projet.

Il suffit ensuite d'importer glad.h (avant glfw3.h) puis d'appeler la fonction gladLoadGL après la création d'un contexte GL :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Pour générer glad.h : https://glad.dav1d.de/
//   C/C++, gl 4.5, OpenGL, Core, extensions: add all, local files
#include "glad.h"

#include <GLFW/glfw3.h>

...

class MyApp
{
    ...
    MyApp()
    {
        ...

        glfwMakeContextCurrent (m_window);

        // Initialisation de la machinerie GL en utilisant GLAD.
        gladLoadGL(); 
        std::cout << "Loaded OpenGL "
            << GLVersion.major << "." << GLVersion.minor << std::endl;
    ...

5.4. Création des objets

OpenGL manipule de nombreux objets (buffers, shaders, etc) qui sont désignés par des identifiants entiers. Dans la terminologie il sont appelés des noms (OpenGL names), ou encore des handles.

Les fonctions glCreate*, qui créent un objet, renvoient un identifiant de type GLuint ; certaines renvoie un GLint pour signaler une erreur avec la valeur -1.

Lorsqu'on a créé un objet, pour l'utiliser il faut ensuite le lier ou le rendre actif en appelant la fonction glBind* ou glEnable* correspondante.

Dans nos exemples, on regroupe la création des objets GL dans la méthode initGL, qui est appelée par le constructeur de MyApp à la fin :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
...
class MyApp
{
    void initGL()
    {
        ...
    }

public:

    MyApp()
    {
        ...

        initGL();
    }

    ...

Lorsqu'il y en aura besoin, on regroupera la destruction des objets GL dans la méthode tearGL (tear signifie détruire), qui sera appelée dans le destructeur de MyApp.

5.5. Shaders de base

Pour OpenGL, un shader est une chaîne de caractères terminée par 0. Un shader peut être chargé depuis un fichier, ou mémorisé dans une chaîne de caractères constante, ou encore généré au vol.

Les shaders d'OpenGL (et Vulkan) sont écrits dans le langage GLSL :

  • compilé par le driver,
    • avant exécution ou au runtime,
    • dans un format de bytecode appelé SPIR-V : bytecode décompilable, utilisé par la plupart des langages de shaders actuels (GLSL, HLSL, ...). ;
  • syntaxe du C, sans les pointeurs ;
  • récursion interdite.

Vertex Shader : voici un shader pass-through, qui se contente de transmettre les coordonnées du sommet et sa couleur :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#version 330 
in vec4 vPos; 
in vec3 vCol; 
out vec3 color; 

void main() 
{ 
    gl_Position = vPos;
    color = vCol; 
}
  • Ligne 1 : le numéro de version d'OpenGL minimal demandé est 3.3 (on aurait mis 450 pour la version 4.5, etc ; depuis la version 330, la version de GLSL correspond à la version d'OpenGL). Il est obligatoire de mettre une version car sinon, OpenGL suppose que c'est la version 110, ce qui empêche d'utiliser le core profile.

  • Lignes 2 à 4 : déclaration des variables globales in et out reçues et transmises par le shader. Les types vec3 et vec4 sont prédéfinis, il s'agit de tableaux de float de taille 3 et 4.

  • Ligne 6 : chaque shader doit avoir une fonction main. Celle-ci sera appelée pour chaque sommet.

  • Ligne 8 : la variable prédéfinie gl_Position reçoit la position calculée.

Fragment Shader : voici un shader pass-through, qui recopie la couleur du fragment :

1
2
3
4
5
6
7
8
#version 330 
in vec3 color; 
out vec4 fragColor;

void main() 
{ 
    fragColor = vec4(color, 1.0); 
} 
  • Ligne 7, la variable en sortie fragColor reçoit la couleur calculée.

  • L'opération vec4(color, 1.0) permet de fabriquer un vec4 à partir d'un vec3 en rajoutant une coordonnée (ici l'opacité à 1.0).

Voir l'exemple fw21-shader.cpp.

5.6. Shader program

Pour envoyer un shader à OpenGL, on commence par déclarer le source dans une chaîne de caractère :

const char* m_vertex_shader_text =
    "#version 330\n"
    ...
    "void main()\n"
    "{\n"
    ...
    "}\n";

La suite est effectuée dans la méthode initGL :

1
2
3
const GLuint vertex_shader = glCreateShader (GL_VERTEX_SHADER);
glShaderSource (vertex_shader, 1, &m_vertex_shader_text, NULL);
compile_shader (vertex_shader, "vertex");
  • Ligne 1 : on crée un objet shader et on obtient un identifiant vertex_shader.
  • Ligne 2 : on transmet le source m_vertex_shader_text pour l'identifiant vertex_shader.
  • Ligne 3 : on compile le shader avec la méthode compile_shader.

La méthode compile_shader fait appel à glCompileShader ci-dessous ligne 4. La suite du code demande si la compilation a réussi ligne 7, puis affiche les messages d'erreurs éventuels lignes 10 à 18 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void compile_shader (GLuint shader, const char* name)
{
    std::cout << "Compile " << name << " shader...\n";
    glCompileShader (shader);

    GLint isCompiled = 0;
    glGetShaderiv (shader, GL_COMPILE_STATUS, &isCompiled);
    if (isCompiled == GL_FALSE) m_ok = false;

    GLsizei maxLength = 2048, length;
    char infoLog[maxLength];
    glGetShaderInfoLog (shader, maxLength, &length, infoLog);

    if (length == 0) return;
    if (isCompiled == GL_TRUE)
         std::cout << "Compilation messages:\n";
    else std::cout << "### Compilation errors:\n";
    std::cout << infoLog << std::endl;
}

Exemple de message obtenu si on oublie un ; :

Compile vertex shader...
### Compilation errors:
0(6) : error C0000: syntax error, unexpected reserved word "void", expecting ',' or ';' at token "void"

On fait de même pour le Fragment Shader :

1
2
3
const GLuint fragment_shader = glCreateShader (GL_FRAGMENT_SHADER);
glShaderSource (fragment_shader, 1, &m_fragment_shader_text, NULL);
compile_shader (fragment_shader, "fragment");

Exemple de message obtenu si au lieu de out vec4 fragColor on utilise la variable dépréciée gl_FragColoc :

Compilation messages:
0(6) : warning C7533: global variable gl_FragColor is deprecated after version 120

La liste des shaders utilisés lors d'un rendu est appelé un shader program. On déclare le shader program de cette façon :

1
2
3
4
m_program = glCreateProgram();
glAttachShader (m_program, vertex_shader);
glAttachShader (m_program, fragment_shader);
link_program (m_program);

La méthode link_program appelle la fonction glLinkProgram pour fabriquer l'exécutable qui sera envoyé au GPU, ci-dessous ligne 4. Cette fonction peut échouer pour différentes raisons (fonction main absente, variables in/out absentes ou trop nombreuses, etc), voir ligne 7. Les messages obtenus ligne 12 sont affichés lignes 14 à 18 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void link_program (GLuint program)
{
    std::cout << "Link program...\n";
    glLinkProgram (program);

    GLint status;
    glGetProgramiv (program, GL_LINK_STATUS, &status);
    if (status == GL_FALSE) m_ok = false;

    GLsizei maxLength = 2048, length;
    char infoLog[maxLength];
    glGetProgramInfoLog (program, maxLength, &length, infoLog);

    if (length == 0) return;
    if (status == GL_TRUE)
         std::cout << "Linking messages:\n";
    else std::cout << "### Linking errors:\n";
    std::cout << infoLog << std::endl;
}

Exemple de message obtenu si on ne met pas de fonction main dans le Fragment Shader :

### Linking errors:
Fragment info
-------------
(0) : error C3001: no program defined

Après cette étape, on peut récupérer l'identifiant des variables d'entrée vPos et vCol du Vertex Shader. Cet identifiant est appelé location ; il sera utilisé lors des dessins pour relier les données (positions et couleurs) aux variables des shaders.

m_vPos_loc = glGetAttribLocation (m_program, "vPos");
m_vCol_loc = glGetAttribLocation (m_program, "vCol");

5.7. Dessins avec VAA

On ne peut plus utiliser le mode de rendu immédiat glBegin/glVertex/glEnd. OpenGL fournit deux moyens pour transmettre des sommets et des couleurs :

  • Un Vertex Attribute Array (VAA) : il s'agit d'une structure côté client, qui sert à préciser l'organisation des données et la location de la variable correspondante dans le shader ; les données sont envoyées lors de chaque rendu avec glDrawArrays.

  • Un Vertex Buffer Object (VBO) : c'est un buffer stocké côté serveur ; le gros avantage est qu'il peut être mémorisé au démarrage du programme, il n'y a plus besoin de renvoyer les données lors des rendus, ce qui économise la bande passante du bus de la carte graphique.

Envoyer des sommets via un VAA est assez simple, c'est ce qui se faisait au "début" des shaders. Mais avec le profile core c'est devenu interdit (la fenêtre reste vide) : il faut obligatoirement stocker les données dans un ou plusieurs VBO. Comme c'est un peu plus compliqué à mettre en œuvre nous le verrons au prochain cours.

Voici le début typique du processus de rendu dans la méthode displayGL :

1
2
3
4
5
6
void displayGL()
{
    //glClearColor (0.95, 1.0, 0.8, 1.0);
    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram (m_program);

On vide le framebuffer et le Z-buffer ligne 4 (la ligne 3 permet de changer la couleur de fond). Ligne 5 on active le shader program en invoquant glUseProgram.

On déclare ensuite les coordonnées des sommets et leurs couleurs dans des tableaux :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    float y = 0.9-m_anim_y;     // pour faire une animation
    GLfloat vertices[] = {
       -0.7, -0.5, -0.1,
        0.8, -0.2, -0.1,
        0.1,    y,  0.3,
       -0.6,  0.7, -0.2,
        0.8,  0.8, -0.2,
        0.1, -0.9,  0.7
    };

    GLfloat colors[] = {
        1.0, 0.6, 0.6,
        1.0, 0.6, 0.6,
        1.0, 0.6, 0.6,
        1.0, 0.0, 0.0,
        0.0, 1.0, 0.0,
        0.0, 0.0, 1.0
    };

On crée ensuite deux VAA avec la fonction glVertexAttribPointer, qui prend les paramètres suivants :

  • GLuint index : la location de la variable dans le shader, qui recevra les coordonnées ou la couleur d'un sommet ;
  • GLint size : le nombre de valeurs par sommet ou couleur (entre 1 et 4) dans le tableau ;
  • GLenum type : le type d'une valeur (en général GL_FLOAT) dans le tableau ;
  • GLboolean normalized : GL_TRUE s'il faut normaliser les données entre 0.0 et 1.0 ;
  • GLsizei stride : le nombre d'octets séparant 2 sommets ou couleurs dans le tableau ; un 0 indique qu'elles sont consécutives en mémoire ;
  • const void* pointer : l'adresse de base du tableau.

Cela donne :

1
2
    glVertexAttribPointer (m_vPos_loc, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    glVertexAttribPointer (m_vCol_loc, 3, GL_FLOAT, GL_FALSE, 0, colors);

Enfin, on active les VAA pour faire les dessins :

1
2
3
4
5
6
7
    glEnableVertexAttribArray (m_vPos_loc); 
    glEnableVertexAttribArray (m_vCol_loc);

    glDrawArrays (GL_TRIANGLES, 0, 6);

    glDisableVertexAttribArray (m_vPos_loc);
    glDisableVertexAttribArray (m_vCol_loc);

La fonction glEnableVertexAttribArray active le VAA pour alimenter la variable du shader correspondante avec les données ; la fonction glDisableVertexAttribArray désactive le VAA.

La fonction glDrawArrays réalise les dessins à partir des sommets données en indice, avec les paramètres :

  • GLenum mode : la nature du dessin ; les valeurs possibles sont à peu près les mêmes que pour glBegin (sans les polygones et quads) ;
  • GLint first : l'indice du premier sommet ou couleur dans le tableau ;
  • GLsizei count : le nombre de sommets ou couleurs.

Le code complet de notre exemple est dans le fichier fw21-shader.cpp.

5.8. Matrices de transformation

OpenGL "moderne" a également supprimé les 3 piles de matrices et toutes les opérations associées :

  • glMatrixMode, glPushMatrix, glPopMatrix
  • glFrustum, glPerspective, glOrtho, glOrtho2D
  • glLoadIdentity, glRotatef, glTranslatef, glScalef
  • gluLookAt

Il faut maintenant définir ces matrices et les transmettre aux shader, pour placer la caméra, paramétrer la projection et opérer des transformations géométriques sur les primitives.

Heureusement, il existe de nombreuses librairies ou modules pour définir et manipuler ces matrices, par exemple la librairie GLM.

Dans ce cours nous allons utiliser le module vmath.h, fourni avec le code source du livre de référence OpenGL Red Book. Son avantage est que toute la librairie est contenue dans un seul fichier .h.

L'idée générale est de déclarer une matrice identité 4x4 au début de displayGL :

vmath::mat4 matrix = vmath::mat4::identity();

puis de lui appliquer les différents calculs, y compris la correction du facteur d'aspect :

GLfloat hr = m_radius, wr = hr * m_aspect_ratio;
matrix = matrix * vmath::frustum (-wr, wr, -hr, hr, 1.0, 5.0);

vmath::vec3 eye {0, 0, 3.0}, center {0, 0, 0}, up {0, 1., 0};
matrix = matrix * vmath::lookat (eye, center, up);

// Rotation de la scène pour l'animation, tout en float pour le template
matrix = matrix * vmath::rotate (m_anim_angle, 0.f, 1.f, 0.15f);

Il faut ensuite transmettre cette matrice au Fragment Shader. Il existe une catégorie spéciale de variables, qui permet de transmettre une même valeur à tous les shaders de façon identique : ce sont les variables uniform.

Nous allons "envoyer" la matrice matrix dans le Fragment Shader sous la forme d'une variable uniform nommée matMVP dans le shader (MVP pour Modèle Vue Projection).

On récupère l'identifiant de matVP dans initGL en appelant glGetUniformLocation :

m_matMVP_loc = glGetUniformLocation (m_program, "matMVP");

puis dans paintGL, après le calcul de matrix, on rend accessible la variable uniform en appelant glUniformMatrix4fv :

glUniformMatrix4fv (m_matMVP_loc, 1, GL_FALSE, matrix);

Le Vertex Shader devient :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#version 330
in vec4 vPos;
in vec3 vCol;
out vec3 color;
uniform mat4 matMVP;

void main()
{
    gl_Position = matMVP * vPos;
    color = vCol;
};

La matrice est reçue ligne 5 ; la transformation géométrique est appliquée au sommet ligne 9.

Le code complet de l'exemple est dans le fichier fw22-matrix.cpp.

Deux exemples supplémentaires sont fournis :

  • l'exemple fw23-wirecube.cpp déplace le dessin des deux triangles dans une classe Triangles, et rajoute une classe WireCube pour dessiner un cube repère ;

  • l'exemple fw24-proj.cpp permet de choisir au clavier le type de projection et de la paramétrer.

Dans ce dernier exemple, le calcul de la projection est regroupé dans la méthode :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void set_projection (vmath::mat4& matrix)
{
    matrix = vmath::mat4::identity();

    GLfloat hr = m_cam_r, wr = hr * m_aspect_ratio;
    switch (m_cam_proj) {
    case P_ORTHO :
        matrix = matrix * vmath::ortho (-wr, wr, -hr, hr, m_cam_near, m_cam_far);
        break;
    case P_FRUSTUM :
        matrix = matrix * vmath::frustum (-wr, wr, -hr, hr, m_cam_near, m_cam_far);
        break;
    default: ;
    }

    vmath::vec3 eye {0, 0, (float)m_cam_z}, center {0, 0, 0}, up {0, 1., 0};
    matrix = matrix * vmath::lookat (eye, center, up);

    // Rotation de la scène pour l'animation, tout en float pour le template
    matrix = matrix * vmath::rotate (m_anim_angle, 0.f, 1.f, 0.15f);
}

Les étapes dans paintGL deviennent :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void displayGL()
{
    //glClearColor (0.95, 1.0, 0.8, 1.0);
    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glUseProgram (m_program);

    vmath::mat4 matrix;
    set_projection (matrix);
    glUniformMatrix4fv (m_matMVP_loc, 1, GL_FALSE, matrix);

    // Dessins

    Triangles triangles {m_vPos_loc, m_vCol_loc};
    triangles.draw();

    if (m_cube_color > 0) {
        WireCube wire_cube {m_cube_color, 0.5, m_vPos_loc, m_vCol_loc};
        wire_cube.draw();
    }
}

Si on veut réaliser des transformations géométriques supplémentaires sur les objets affichés, il suffit de copier matrix dans une variable temporaire matrix2 (équivalent de glPushMatrix/glPopMatrix), multiplier matrix2 par la matrice de transformation, puis appeler glUniformMatrix4fv pour transmettre matrix2 au Vertex Shader, et faire le dessin :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    vmath::mat4 matrix2;

    matrix2 = matrix;
    matrix2 = matrix2 * vmath::rotate(...);
    matrix2 = matrix2 * vmath::translate(...);
    glUniformMatrix4fv (m_matMVP_loc, 1, GL_FALSE, matrix2);
    // dessin...

    matrix2 = matrix;
    matrix2 = matrix2 * vmath::rotate(...);
    matrix2 = matrix2 * ...
    glUniformMatrix4fv (m_matMVP_loc, 1, GL_FALSE, matrix2);
    // dessin...

    ...