Master Informatique 2018-2019
M3DS



TP 01 : Introduction OpenGL

Objectifs :

1  Prise en main et consignes

Question 1. Inscrivez vous sur moodle au cours M3DS : http://moodle.univ-lille1.fr/course/view.php?id=413 (demander la clef à l’enseignant).

Dans cette première partie on se contente de découvrir le squelette des TPs. Cette prise en main sera commune à tous les TPs de M3DS.

Question 2. Récupérez introOpenGL.zip. Décompressez-le pour obtenir introOpenGL_Student1_Student2 qui sera votre dossier de travail.

Question 3. Sans attendre :

  1. renommez ce dossier en remplaçant Student1_Student2 avec votre nom (les 2 noms si vous êtes en binôme).
  2. renommez le fichier .pro sur le même principe.
  3. indiquez dès à présent vos noms/prénoms dans le Readme.txt

Pour rendre un tp, vous devrez en effet remettre le dossier complet (archive .zip) avec ces renommages.

Question 4. Le fichier .pro est la configuration du projet sous le framework Qt :

  1. Dans un terminal, lancez la commande qtcreator et ouvrez le .pro avec le menu Fichier -> Ouvrir un fichier ou un projet. Une fenêtre s’ouvre : cliquez directement sur Terminé.

    Cette configuration est mémorisée par QtCreator dans le fichier .pro.user... : ne vous en préoccupez pas (l’ouverture du projet se faisant toujours avec le .pro). Lors d’un changement de machine, il faudra regénérer cette configuration (message de confirmation si un .pro.user existe déjà).

  2. Vous devez être maintenant sous l’éditeur (avec la liste des fichiers du projet à gauche). Exécutez le projet en appuyant sur <Ctrl>+R (lance également la compilation) : une fois l’application du tp lancée, vous devez voir apparaitre la fenêtre graphique suivante.
  3. Vous pouvez quitter l’application du tp en appuyant simplement sur la touche <Escape>.

Remarque : sous QtCreator, pour avoir directement la référence (très utile !) d’une instruction OpenGL ou C++ en appuyant sur <F1> (aide contextuelle) : récupérez cppreference-doc-en-cpp.qch et gl42.qch, puis, sous QtCreator menu outils -> options... -> aide (sous-menu de la fenêtre) -> onglet documentation -> bouton ajouter... et sélectionnez le fichier .qch

Question 5. Dans la liste à gauche, les fichiers sont organisés selon les En-têtes (les .h qui contiennent la déclaration des classes), les Sources (les .cpp qui contiennent l’implémentation) et... les Autres fichiers (contiendra les médias nécessaires : les shaders, les images,...). Ouvrez la liste Sources : vous voyez le dossier src où se trouvent tous les fichiers. src est organisé en 2 sous dossiers :

Ouvrez GLApplication.cpp : repérez les méthodes initialize, resize, update, draw, ainsi que le constructeur GLApplication::GLApplication communs à tous les tps (dont le rôle a été décrit en cours).

2  Tracé simple

Question 6. On rappelle que pour un tracé en OpenGL, il faut réaliser les étapes suivantes :

Bien que le code est déjà intégré dans le squelette (pour gagner du temps), vous devez parfaitement comprendre toute la démarche (référez vous au cours) ainsi que le code associé à GLApplication::initTriangleBuffer, GLApplication::initTriangleVAO, GLApplication::draw et le code des 2 shaders simple.vert et simple.frag (en sachant où ils interviennent dans le tracé). Pour l’initialisation des shaders (GLApplication::initProgram, comprenez la démarche (création d’un vertex shader, d’un fragment shader, lecture/compilation/link des fichiers GLSL sources), et comprenez bien le glBindAttribLocation qui y apparait (i.e. comprendre le lien entre le VAO et le program shader).

Vous devrez intervenir dans ces méthodes dans le reste du TP.

Question 7. Dans le constructeur GLApplication::GLApplication sont affectées les données de l’application : ce sont 2 tableaux contenant les coordonnées des sommets de 2 triangles d’une part (membre _trianglePosition), et la couleur en chacun des sommets d’autre part (membre _triangleColor; non pris en compte pour l’instant : la couleur est pour l’instant fixée directement dans le fragment shader simple.frag). Remarque : les différents membres sont déclarés dans GLApplication.h.

Un seul triangle est tracé à l’exécution : pour obtenir les 2 triangles, trouvez et corrigez les erreurs dans les méthodes GLApplication::initTriangleBuffer, GLApplication::initTriangleVAO, GLApplication::draw.

Question 8. Assurez vous de pouvoir répondre à : Quel est le rôle d’un VBO ? d’un VAO ? d’un Program Shader ?

Question 9. Sur le résultat, désignez les sommets vertex 0, 1, 2, 3, 4, 5 ? (cf affectation de _trianglePosition dans GLApplication::GLApplication).

Question 10. On souhaite que le tableau _triangleColor (affecté dans GLApplication::GLApplication) correspondent à une couleur pour chaque sommet. Le remplissage du triangle se fera alors par interpolation (cf image ci-dessous). Pour en tenir compte il faut :

Appuyez vous, bien sûr, sur ce qui est déjà fait pour la position des sommets (attention, cependant, aux copier-coller trop directs). Testez

Question 11. Comprenez parfaitement la notion de varying pour passer des valeurs du vertex au fragment shader ainsi que l’interpolation linéaire associée (appuyez vous sur le cours et le résultat du tp).

Question 12. Remplacez (quand cela est pertinent, prenez l’habitude de mettre en commentaire l’ancien code plutôt que de le supprimer) l’initialisation de _trianglePosition dans GLApplication::GLApplication par (il s’agit d’une permutation des sommets) :

_trianglePosition = { -0.8,-0.5,0.0, // vertex 0 anciennement vertex 0 0.8,0.5,0.0, // 1 anciennement 4 -0.5,0.5,0.0, // 2 anciennement 2 -0.2,-0.5,0.0, // 3 anciennement 1 0.5,-0.5,0.0, // 4 anciennement 5 0.2,0.5,0.0 // 5 anciennement 3 };

Testez et comprenez le résultat (comprenez notamment que glDrawArrays trace dans l’ordre des données qui se trouvent dans le buffer).

Question 13. Pour retrouver nos 2 triangles précédents, sans modifier les données, il faut tracer les sommets selon quel ordre ? (donnez la liste d’indices).

Question 14. Pour faire cela, on va indiquer à OpenGL l’ordre dans lequel il faut prendre les sommets avec un tableau d’indices (cf transparent Indexed Face Set dans le cours).

  1. Il faut d’abord définir le tableau d’indices donnant l’ordre des sommets : déclarez std::vector<unsigned int> _elementData dans GLApplication.h (c’est un tableau d’entiers positifs; remarque : sous QtCreator on passe rapidement du .h au .cpp en appuyant sur <F4>), et affectez le avec les valeurs de la question précédente dans le constructeur GLApplication::GLApplication (appuyez vous sur la syntaxe des autres tableaux).
  2. On va recopier ces indices dans un buffer OpenGL : déclarez un GLuint _elementBuffer dans le .h puis dans le GLApplication::initTriangleBuffer on fait la copie comme pour les autres buffers (mais attention ! notez bien la cible du bind et du bufferData dans le code ci-dessous : il s’agit de GL_ELEMENT_ARRAY_BUFFER, et non de GL_ARRAY_BUFFER. Cette cible est utilisée spécifiquement lors d’un glDrawElements qui doit exploiter un tableau d’indice pour connaitre l’ordre des sommets) :
    glGenBuffers(1,&_elementBuffer); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,_elementBuffer); glBufferData(GL_ELEMENT_ARRAY_BUFFER,_elementData.size()*sizeof(unsigned int),_elementData.data(),GL_STATIC_DRAW);
  3. Il faut ajouter au VAO (dans GLApplication::initVAO) l’ordre des indices de sommets lors du tracé. Il suffit de faire un glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,...) lorsque le VAO souhaité est actif (donc encore attention à la cible du bind !; notez bien également que _elementBuffer n’a aucun rapport avec les attributs de sommet : il n’y a donc pas de glVertexAttribPointer/glEnableVertexAttribArray associé).
  4. Enfin, la commande de tracé devient glDrawElements au lieu de glDrawArrays : glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0); (cf doc pour le détail des paramètres).

Testez

Le fait de spécifier ainsi l’ordre avec un tableau d’indice s’appelle Indexed Vertex Arrays ou Indexed Face Set.

Question 15. On retrouve bien nos 2 triangles, mais vous devez constater une différence par rapport au résultat précédent (cf image Question 8). Expliquez (Sur quels attributs s’appliquent l’ordre des indices de GL_ELEMENT_ARRAY_BUFFER ?).

Question 16. Changez les coordonnées des sommets avec :

_trianglePosition = { -0.8,-0.8,0.0, 0.8,0.8,0.0, 0.0,0.2,0.0, -0.8,0.8,0.0, 0.8,-0.8,0.0, 0.0,0.2,0.0 };

Testez.

Constatez qu’on a 2 sommets qui ont les mêmes coordonnées (repérez le dans _trianglePosition). On peut éviter cette redondance avec les indexed vertex arrays : il suffit de répéter l’indice lors de la succession des sommets. Ainsi, les indices (0,3,2,2,1,4) donneront bien les 2 triangles souhaités (et le dernier sommet de _trianglePosition devient inexploité).

Modifiez le programme pour avoir ces indices (et supprimez le sommet redondant de _trianglePosition) (notez qu’il n’y a plus que 5 positions de sommets à recopier dans le VBO correspondant !).

Testez. Comparez avec l’image précédente. Expliquez : d’où proviennent les couleurs ?

Comprenez bien que les attributs (position et couleur ici) d’un sommet ne sont pas dissociables. Un indice donné correspond à l’indice pour chaque attribut. La conséquence est que si on souhaite 1 sommet avec 2 couleurs distinctes, on est obligé, pour OpenGL, de dupliquer la position (c’est à dire d’avoir 2 sommets distincts). Cf cours et le transparent "Sommet = tous ses attributs".

Question 17. Modifiez à nouveau :

_trianglePosition = { -0.8,-0.8,0.0, -0.6,0.8,0.0, -0.4,-0.6,0.0, -0.2,0.6,0.0, 0.0,-0.8,0.0, 0.2,0.8,0.0, 0.4,-0.6,0.0, 0.6,0.6,0.0, 0.8,-0.8,0.0 }; // tous les sommets à rouge : _triangleColor.clear(); for(unsigned int i=0;i<9;++i) { _triangleColor.push_back(1);_triangleColor.push_back(0);_triangleColor.push_back(0);_triangleColor.push_back(1); }

et testez en remplaçant le glDrawElements par un glDrawArrays(GL_TRIANGLES,0,9);. Constatez les 3 triangles (cf image question suivante pour la disposition). Remarque : notez que les indices du GL_ELEMENT_ARRAY_BUFFER sont maintenant ignorés car l’instruction glDrawArrays ne l’exploite pas (trace en prenant simplement les sommets dans l’ordre du buffer).

Question 18. Pour mieux voir ce qui suit, mettez un GL_LINE au lieu de GL_FILL pour l’instruction glPolygonMode(GL_FRONT_AND_BACK,GL_FILL) (se trouve dans GLApplication::initialize). Tous les tracés se feront alors sans remplissage des triangles (seuls les pixels du "bord" des triangles sont tracés; ils subissent bien sûr le fragment shader). La notion de FRONT et BACK sera vue plus tard en cours. Testez.

Question 19. Remplacez à présent le glDrawArrays(GL_TRIANGLES,0,9) par glDrawArrays(GL_TRIANGLE_STRIP,0,9). Testez. Comprenez alors le résultat d’un tracé avec GL_TRIANGLE_STRIP en essayant glDrawArrays(GL_TRIANGLE_STRIP,0,3), puis ensuite 4, puis 5, ... etc. (constatez que chaque sommet trace un nouveau triangle à partir des 2 sommets précédents). Ceci est souvent utilisé pour faire des bandes de triangles.

Question 20. Modifiez _trianglePosition et le nombre de sommets de glDrawArrays(GL_TRIANGLE_STRIP,...) pour obtenir (en exploitant impérativement GL_TRIANGLE_STRIP) :

Prenez un snapshot de votre résultat (click sur le bouton de l’interface;les captures d’écran vont dans media/snapshot).

Pensez à prendre des snapshots pour illustrer les différentes questions du TP ! (de même que pour les TPs suivants).

Question 21. Ajoutez une méthode GLApplication::initStrip(int nbSlice,float xmin,float xmax,float ymin,float ymax) dans GLApplication.cpp (en C++, il faut également faire la déclaration dans le GLApplication.h) qui initialise _trianglePosition pour faire une bande similaire à l’exercice précédent (donc en exploitant GL_TRIANGLE_STRIP) mais avec nbSlice lignes verticales (il y en avait 4 à l’exercice précédent). Les paramètres xmin, xmax, ymin et ymax définissent les extrémités de la bande. Remarque : pour ajouter un float au tableau _trianglePosition il suffit de faire un _trianglePosition.push_back(valeur) (faire un _trianglePosition.clear() pour faire un tableau vide au début de la méthode). Pour un sommet, il faut donc faire 3 push_back (pour x, y et z=0).

Pour l’attribut de couleur, faire également une boucle en mettant tous les sommets avec une couleur identique.

Pour le nombre de sommets à donner au glDrawArrays on peut le déduire du nombre d’éléments dans le tableau : _trianglePosition.size(). Faut il modifier la copie des VBO (méthode initTriangleBuffer()) ? l’initialisation du VAO (méthode initTriangleVAO()) ? le program shader (initProgram()) ?

Comprenez les conséquences de la modification du nombre de sommets dans _trianglePosition, _triangleColor, etc, sur le reste du code.

Testez (n’oubliez pas bien sûr d’appeler initStrip depuis le constructeur GLApplication::GLApplication).

Question 22. Passez en GL_FILL et modifiez l’affectation de _triangleColor de GLApplication::initStrip pour obtenir :

GL_FILLGL_LINE

(sommets du "haut" sont en dégradés de bleu, c’est à dire que la composante bleu varie entre 1 et 0; les sommets du "bas" sont en vert variant de 0 à 1).

Question 23. Faire une méthode GLApplication::initRing(int nbSlice,float r0,float r1) qui permet d’obtenir :

Indications : remarquez qu’il s’agit exactement de la même configuration que pour initStrip (il s’agit également d’une bande de triangle). Il faut seulement modifier le calcul des coordonnées (i.e. faire nbSlice*2 sommets en faisant varier un angle de 0 à 2π plutôt que de faire varier un x de xmin à xmax). r0 correspond au rayon intérieur, et r1 correspond au rayon extérieur. On répartit les sommets sur un tour complet en faisant varier un angle theta entre 0 et 2π (prendre 3.14159 pour π). L’angle 0 correspond à l’horizontale (là où il y a la discontinuité des couleurs). Pour obtenir le sommet du cercle intérieur qui se trouve à l’angle theta, il suffit de calculer (idem pour les sommets extérieurs, mais avec r1) :



x = r0cos(θ)
y = r0sin(θ)

(équation paramétrique d’un cercle).

Testez

3  Uniform

Question 24. L’objectif est d’obtenir la variation du rayon comme sur cette vidéo :

(sur la vidéo il s’agit d’une valeur de couleur constante pour les sommets intérieurs et extérieurs; ne vous en préoccupez pas et conservez la visualisation que vous avez actuellement).

Pour obtenir cela il suffit de modifier les coordonnées des sommets en les multipliant par un certain facteur coeff (i.e changement d’échelle par rapport à l’origine). Mais plutôt que de modifier les données entre chaque image (i.e. modifier tout le tableau _trianglePosition par l’application), nous allons dédier cette tâche au vertex shader (i.e. exécuté par OpenGL/la carte graphique; les données de l’application C++ restant inchangées).

Modifiez le vertex shader pour qu’il prenne en paramètre un uniform float coeff. Déclarez alors une variable locale dans le main du vertex shader vec3 newPosition=position (nécessaire car on ne peut pas modifier position qui est un attribut). Il reste à multiplier newPosition par le coefficient. Notez la souplesse du type vec3 en GLSL : on peut manipuler les coordonnées dans leur ensemble : newPosition=position*coeff fonctionne (multiplie chaque coordonnée par coeff). Pour information, on peut également sélectionner des groupes de coordonnées : par exemple newPosition.xy=position.xy*coeff; newPosition.xz=position.xz*coeff, etc.

L’application C++ doit se charger de passer la valeur coeff souhaitée au shader avant chaque glDraw (le program shader a pour identifiant _shader0). Par exemple :

glUniform1f(glGetUniformLocation(_shader0,"coeff"),_coeff);

Vous ferez évoluer le membre float _coeff (qu’il faut déclarer dans GLApplication.h) dans GLApplication::update pour qu’il varie entre 0 et 1 (puis de 1 à 0, etc). Rappel : update() est appelée constamment par Qt avant un appel à draw() (toutes les 20ms environ). Le rôle du update est de mettre à jour les données de l’application entre chaque image; on s’interdit, par convention, d’y placer du code OpenGL (tout comme on s’interdit de faire évoluer les données de l’application dans le GLApplication::draw).

Testez. (vous devez obtenir un résultat similaire à la vidéo)

4  Texture

Question 25. (vous pouvez désactiver l’animation pour la suite, en passant, par exemple une valeur constante pour _coeff)

Nous ajoutons à présent une texture. Tout d’abord le chargement de l’image de texture en mémoire OpenGL est déjà assuré par le squelette (i.e. cf GLApplication::initTexture).

On commence par affecter les coordonnées de textures en chaque sommet (on ajoute donc un attribut à chaque sommet : les coordonnées de texture).

Pour la mise en place, et la compréhension, on revient, pour le moment, à un objet simple : ajoutez à la fin de votre constructeur GLApplication::GLApplication (ne modifiez pas le reste de vos initialisations de GLApplication::GLApplication) :

_trianglePosition = { // rectangle tracé avec TRIANGLE_STRIP -0.6,-0.8,0, -0.6,0.8,0, 0.6,-0.8,0, 0.6,0.8,0 }; _triangleColor = { // tous les sommets en rouge 1,0,0,1, 1,0,0,1, 1,0,0,1, 1,0,0,1, }; _triangleTexCoord = { // coordonnées de texture en chaque sommet 0,1, 0,0, 1,1, 1,0 };

(testez pour vérifier que vous obtenez bien un rectangle rouge).

Il faut tenir compte de ces coordonnées de texture _triangleTexCoord dans le vertex shader. On procède exactement comme pour les autres attributs (i.e. position et color):

Testez : cela ne doit rien changer pour l’instant (on n’a pas encore affecté la couleur des pixels dans le fragment shader pour tenir compte de la texture; vous devez donc toujours avoir un rectangle), mais cela permet de vérifier que vos modifications ne génèrent pas d’erreurs...

Question 26. Il faut à présent affecter la couleur du pixel (i.e. fragColor du fragment shader) avec la couleur du pixel de la texture (appelé texel) qui se trouve aux coordonnées fTexCoord. Pour cela, il faut, dans le fragment shader, définir un uniform (uniform sampler2D textureUnit par exemple) pour faire référence à l’unité de texture souhaitée. Puis il suffit de faire fragColor=texture(textureUnit,fTexCoord); pour lire la valeur du texel et l’affecter à fragColor.

Enfin (!), dans GLApplication::draw, il faut indiquer quelle est la valeur du uniform sampler2D textureUnit (assurez vous impérativement de comprendre le code que vous êtes en train de copier/coller, notamment le rôle et le lien entre identifiant de texture et unité de texture) :

glActiveTexture(GL_TEXTURE0); // on travaille avec l'unité de texture 0 // dans l'instruction suivante, _textureId correspond à l'image "lagoon.jpg"; cf GLApplication::initTexture pour l'initialisation de _textureId glBindTexture(GL_TEXTURE_2D,_textureId); // l'unité de texture 0 correspond à la texture _textureId // (le fragment shader manipule des unités de textures et non les identifiants de texture directement) glUniform1f(glGetUniformLocation(_shader0,"textureUnit"),0); // on affecte la valeur du sampler2D du fragment shader à l'unité de texture 0. glDrawArrays(GL_TRIANGLE_STRIP,0,_trianglePosition.size()/3);

Testez : vous devez voir maintenant le rectangle (avec une texture "à l’envers") :

Question 27. Modifiez (uniquement) les coordonnées de texture du tableau _triangleTexCoord de l’application pour avoir l’image "à l’endroit". Testez et assurez vous que vous comprenez le rôle des coordonnées de texture attribuées à chaque sommet.

Question 28. Puis, modifiez à nouveau les coordonnées de texture pour n’avoir que la moitié de l’image sur l’ensemble du rectangle (n’hésitez pas, bien sûr, à tester d’autres valeurs de coordonnées de texture jusqu’à compréhension parfaite).

Question 29. Assurez vous que toute la démarche pour la mise en place des textures est claire : coordonnées de textures au sommet (nouvel attribut du vertex shader); à quoi correspondent les valeurs de coordonnées de texture par rapport à l’image de texture; comprendre l’interpolation des coordonnées de textures pour chaque pixel; l’accès aux valeurs de la texture par texture(textureUnit,fTexCoord) dans le fragment; la notion d’unité de texture et son lien avec un identifiant de texture.

Question 30. Revenez avec l’initialisation de l’anneau et modifiez GLApplication::initRing en initialisant _triangleTexCoord pour obtenir (i.e. toute l’image est plaquée sur l’ensemble de l’anneau) :

Question 31. Changez maintenant les coordonnées de texture pour obtenir (indications : les coordonnées de texture dépendent directement des coordonnées (x,y) des sommets; attention cependant à bien reporter les coordonnées de texture entre 0 et 1) :

Question 32. Dans le fragment shader, on peut calculer la couleur finale (i.e. fragColor) de manière très souple. Essayez par exemple :

// rouge de la texture multiplié par le rouge de fColor, idem pour green, et blue. fragColor=texture(textureUnit,fTexCoord)*fColor; // fColor étant la couleur interpolée aux sommets(questions précédentes)

Constatez alors le résultat (mélange des couleurs entre la texture et le fColor).

Question 33. Enfin, testez et comprenez (réactivez l’évolution de coeff, mais désactivez le changement d’échelle par coeff dans le vertex shader pour voir l’effet uniquement sur les couleurs) :

fragColor=texture(textureUnit,fTexCoord)*fColor.b; // coeff = uniform passée par l'application (questions précédentes). fragColor.g*=(1.0-coeff);

Notez qu’il faut redéclarer uniform float coeff; pour le fragment (aura la même valeur que celle du vertex shader; ce n’est pas un varying et n’est donc pas interpolé : coeff est donc identique pour tous les pixels).


Ce document a été traduit de LATEX par HEVEA