TD4 - Pipeline programmable

1 - Introduction

La bibliothèque OpenGL a subit une évolution majeure suite à l'apparition des shaders programmables, et de nombreuses autres évolutions suite à l'importante évolution des GPU ces 15 dernières années. Pour ces raisons, de nombreuses fonctionnalités des premières versions de l'API sont devenues inutiles ou peu adaptées aux architectures modernes. Par conséquent, lors du passage à la version 3 d'OpenGL, un certain nombre de ces fonctionnalités ont été dépréciées et ne devraient plus être utilisées, étant amenées à disparaître. Pour les TDs, nous utiliserons OpenGL 3.3 avec le profile core, ce qui signifie que les fonctionnalités dépréciées sont interdites et provoquent des erreurs à l'exécution en cas d'utilisation.

Attention : de nombreux tutoriaux et exemples disponibles sur internet utilisent les fonctionnalités dépréciées et ne fonctionneront donc pas avec ces TDs. Ce n'est pas un bug. Les sites internet ne mentionnant pas explicitement qu'ils visent OpenGL 3 doivent donc être traités avec méfiance.

Télécharger et compilez le code fourni pghp_td4.zip en utilisant cmake :

> mkdir build
> cd build
> cmake ../pghp_td4
> make

La première étape pour utiliser OpenGL est de créer un context et généralement une fenêtre associée capable d'afficher un rendu OpenGL. Ces deux tâches seront effectuées ici par la bibliothèque GLFW, dont la documentation est disponible à l'adresse : http://www.glfw.org/docs/latest/

Le fichier main.cpp initialise le contexte OpenGL, crée la fenêtre principale et exécute l'application interactive. La fonction main() appelle la méthode Viewer::init après la création de la fenêtre et du contexte OpenGL, Viewer::reshape lorsque la fenêtre est redimensionnée et Viewer::updateScene lorsque l'image doit être redessinée.

2 - Fenêtre d'affichage

fenetre

OpenGL peut être qualifié de machine à états dans le sens où le contexte lié à l’application possède tout au long de l’exécution un état courant qui peut être altéré au travers de nombreuses fonctions. Généralement, pour activer (resp. désactiver) un état du contexte, OpenGL fournie la fonction glEnable (resp. glDisable). D’autres fonctions sont disponibles pour spécifier des états plus complexes (la modification de l’état du viewport, par exemple).

1. Déterminez l’influence de l’appel à la fonction glViewport. Pour cela, changez la taille de la fenêtre après avoir modifié la fonction Viewer::reshape en remplaçant ligne glViewport(0, 0, w, h) par :

  • rien
  • glViewport(0, 0, WIDTH, HEIGHT)
  • glViewport(0, 0, w, HEIGHT)
  • glViewport(w/2, 0, WIDTH, HEIGHT)
  • glViewport(w/2, h/2, WIDTH, HEIGHT)

2. En utilisant glViewport, modifiez la méthode Viewer::display de manière à afficher deux triangles dans la même fenêtre (avant l'appel à glDrawArrays). Le premier s’affichera dans la moitié gauche, le second dans la moitié droite. (Revenir ensuite à un rendu unique.)

3. La valeur de la couleur du fond est spécifiée par la fonction glClearColor. Cette couleur est un état modifiable. Une couleur en OpenGL est définie par 3 composantes : rouge, vert et bleu. Chaque composante est déterminée par un nombre entre 0.0 et 1.0 qui indique l’intensité de la composante. Par exemple, la couleur rouge est spécifiée par le triplet (1.0,0.0,0.0).

Modifiez les methodes keyPressed et display de la classe Viewer de telles sorte que :

  • la touche '1' change la couleur du fond en rouge
  • la touche '2' change la couleur du fond en vert
  • la touche '3' change la couleur du fond en bleu
  • la touche '0' change la couleur du fond en noir

3 - Géométrie et attributs

Regardez le code OpenGL des fonctions createVAO et display de la classe Viewer. La première crée un Vertex Array Object (VAO). Celui-ci permet, lors de l'affichage, d'envoyer en une seule instruction la géométrie (le buffer des sommets Vertex Buffer Object) et ses attributs (sa couleur, dans le cas présent) à la carte graphique. La seconde fonction active le VAO et demande le rendu de la géométrie à la carte graphique (glDrawArrays) en spécifiant le type de primitive à dessiner (triangle, ligne, point, etc.).

1. Changez l'appel à glDrawArrays pour ne dessiner que les sommets du triangle, puis ne dessiner que son contour.

rectangle

Remarque : Vous pouvez contrôler la taille des points à l'aide de la fonction glPointSize à condition de désactiver GL_PROGRAM_POINT_SIZE avec glDisable. Il n'est plus possible de modifier simplement l'épaisseur d'une ligne en OpenGL 3 (il faut dessiner un ruban de triangles pour représenter une ligne épaisse).

2. Modifiez les tableaux vert_data et col_data ainsi que la fonction createVAO pour dessiner un rectangle (à l'aide de deux triangles) dont le quatrième sommet est jaune.

3. Pour éviter de dupliquer la géométrie et ses attributs, vous allez utiliser un tableau d'indices pointant sur chaque sommet. Créez un tableau d'entiers indexant les 6 sommets des 2 triangles. Dans Viewer::createVAO, générez un buffer supplémentaire et activez le buffer des indices :

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _vbo[0]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6*sizeof(GLint), indices,  GL_STATIC_DRAW);

Pour utiliser les indices lors du rendu, remplacez l'appel à glDrawArrays par glDrawElements.

4 - Shaders

pipeline

(d'après LightHouse3D, A. R. Fernandes)

Comme illustré par le diagramme ci-joint, la fonction createShader charge et compile indépendamment les vertex et fragment shaders avant de pratiquer l'édition de lien du programme complet. Les points d'entrées pour les attributs de position et de couleur sont finalement activés.

1. Analysez les vertex et fragment shaders utilisés (également ci-dessous).

// Vertex shader
#version 330 core
in vec3 vtx_position;
in vec3 vtx_color;
out vec3 vcol;

void main(void) {
   vcol = vtx_color;
   gl_Position = vec4(vtx_position, 1.0);
}
// Fragment shader
#version 330 core
in vec3 vcol;
out vec4 fcol;

void main(void) {
    fcol = vec4(vcol,1.0);
}

2. Modifiez le vertex shader pour dessiner un rectangle de taille 1/2. Modifiez le fragment shader pour permuter les canaux de couleur.

3. Observez la classe Shader qui encapsule la gestion des shaders chargés depuis des fichiers textes. Remplacez la fonction createShader par une instance de cette classe, et utilisez la méthode activate de cette instance dans la fonction display. Adaptez également la fonction createVAO pour activer les bons attributs avec glEnableVertexAttribArray (en remplacement des macros POSITION_ATTRIB et COLOR_ATTRIB). Déplacez le code des shaders (vert_shader et frag_shader) dans des fichiers externes (utilisez la macro DATA_DIR contenant le chemin absolu vers le dossier data du projet).

4. Ajoutez un raccourci clavier pour recharger les shaders en cours d'utilisation.