Master Informatique 2018-2019
M3DS



TP 02 : Projection/Depth/Eclairement

Objectifs :

Attention : ce tp est très dirigiste (beaucoup de questions se résument à des copier-coller pour gagner du temps sur les aspects techniques). Il faut comprendre impérativement ce qui est fait ainsi que les différentes notions qui apparaissent.

1  Prise en main

Question 1. Reprenez openGL3D.zip sur le portail.

Question 2.

  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

Question 3. Dans le dossier application, vous avez 2 fichiers : GLApplication.cpp (dont l’organisation est commune à tous les tps et similaire au premier TP), et BasicMesh.cpp qui assurera les opérations de tracé d’objets 3D de ce TP (initialisation des données de l’application/buffers/vao/etc).

Constatez que dans le dossier p3d sont présentes plusieurs classes utilitaires (Shader pour abstraire les aspects techniques liés aux shaders, Vector3 pour manipuler les coordonnées (x,y,z), etc).

Enfin constatez que la présence du shader, pour l’instant rudimentaire, utilisé pour ce TP (openGL3D.vert et openGL3D.frag). Ces shaders sont déjà intégrés par le squelette (cf l’affectation de _shader dans GLApplication::initialize) gràce à la classe Shader (lecture fichiers sources/compilation/link/etc).

Question 4. Allez dans BasicMesh::initTetrahedron et constatez l’organisation des données :

Complétez BasicMesh::initVAO pour avoir en attribut 0 les données de positions et en attribut 1 les données de couleurs depuis l’unique buffer _attributeBuffer (il faut donc exploiter tous les paramètres de glVertexAttribPointer; cf cours pour un exemple).

Testez :

2  Depth Buffer

Question 5. Schématisez, à main levée sur un brouillon, l’objet construit dans BasicMesh::initTetrahedron en reportant les coordonnées de position et en reliant selon _element.

Question 6. Pour mieux comprendre la visualisation obtenue, et en modifiant uniquement les données position, color, _element, affectez une couleur par face : le premier triangle tracé (0,1,2) en rouge, le deuxième (0,2,3) en vert, le troisième (0,1,3) en bleu et le dernier (1,2,3) en cyan (indication : vous devez dupliquer les positions dans le tableau position; le tableau _element se trouve, par conséquent, également modifié : du fait de la duplication des sommets, vous devez avoir à présent 12 indices dans _element). En suivant ces consignes, vous devez obtenir :

Comprenez impérativement la nécessité de dupliquer les coordonnées des sommets (cela est résumé dans le premier cours sur le transparent "1 sommet = tous ses attributs").

Question 7. Prenez à présent les tableaux suivants pour BasicMesh::initTetrahedron :

position={ -1,0,-1, // V0 1,0,-1, // V1 0,1,1, // V2 -1,0,-1, // V0 0,1,1, // V2 0,-1,1, // V3 -1,0,-1, // V0 1,0,-1, // V1 0,-1,1, // V3 1,0,-1, // V1 0,1,1, // V2 0,-1,1 // V3 }; color={ 1,0,0, 1,0,0, 1,0,0, 0,1,0, 0,1,0, 0,1,0, 0,0,1, 0,0,1, 0,0,1, 0,1,1, 0,1,1, 0,1,1 }; _element={ 0,1,2,3,4,5,6,7,8,9,10,11 };

Expliquez, dans le Readme.txt, le résultat obtenu (en vous appuyant sur l’ordre de tracé des triangles sachant qu’il n’y a pas d’élimination des parties cachées).

Question 8. Activez l’élimination des parties cachées par depth buffer :

  1. Dans GLApplication::initialize :
    glEnable(GL_DEPTH_TEST); // activation Depth Buffer (opérations écriture/tests) glDepthFunc(GL_LESS); // le test passe si depth(src) < depth(dst) glClearDepth(1); // valeur d'initialisation du depth destination de tous les pixels lors d'un glClear
  2. Dans GLApplication::draw :
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // affecte tous les pixels du color buffer et du depth buffer avec les valeurs d'initialisation

Testez et comprenez la conséquence :

Question 9.

  1. Changez le test du depth buffer en GL_GREATER au lieu de GL_LESS : testez/expliquez (écran blanc).
  2. Changez à présent le glClearDepth avec la valeur 0 : testez/expliquez (triangles cyan et vert). Attention : on rappelle que la valeur de profondeur manipulée par le depth buffer sont en Window Coordinates dans l’intervalle [0,1]; c’est à dire que les coordonnées [−1,1] en z que vous donnez en Normalized Device Coordinates sont reportées sur [0,1].
  3. Testez avec un glClearDepth(0.5) : expliquez le résultat.
  4. Revenez avec un depth test à GL_LESS et un glClearDepth(1.0) (valeurs "traditionnelles").

3  Volume de visualisation

Question 10. Prenez les données suivantes pour cette partie (i.e. modifiez BasicMesh::initTetrahedron) :

position={ -20,0,-10, // V0 10,0,-10, // V1 0,10,-30, // V2 0,-20,-30 // V3 }; color={ 1,0,0, // rouge 0,1,0, // vert 0,0,1, // bleu 0,1,1 // cyan }; // index for 4 triangles _element={ 0,1,2,0,2,3,0,1,3,1,2,3 };

(si vous testez, vous aurez une fenêtre blanche, puisque tous les triangles du tétraèdre sont hors volume de visualisation (les coordonnées sont effectivement hors [−1,1])).

Nous souhaitons un volume de visualisation plus souples que les coordonnées normalisées NDC. Nous allons donc redéfinir complêtement le volume de visualisation (se référer également au cours) avec :

Nous raisonnerons donc maintenant dans le système de coordonnées Eye (i.e. les positions passées au vertex shader seront des coordonnées dans le repère Eye).

Or à la sortie du vertex shader, il faut nécessairement des coordonnées en Clip Coordinates (coordonnées normalisées en homogènes affectées à gl_Position). Toutes les coordonnées doivent donc subir une « conversion »dans le vertex shader pour traduire les coordonnées données dans le repère Eye vers le repère Clip Coordinates.

Question 11. On convertit les coordonnées Eye en Clip Coordinates, en transformant la position du sommet passé au vertex shader. Cette transformation peut se traduire par une matrice homogène projection (cf cours).

  1. Modifiez le vertex shader pour avoir (bien comprendre l’intégration et l’utilisation de la matrice projection dans le code qui suit) :
    #version 130 in vec3 position; // Eye Coordinates in vec3 color; out vec3 fColor; uniform mat4 projection; void main() { vec4 eyePosition=vec4(position,1); // passage en coordonnées homogènes vec4 clipPosition=projection*eyePosition; // transformation par la matrice de projection fColor=color; gl_Position=clipPosition; // gl_Position doit être donné en clip coordinates }
  2. il reste à passer la valeur de la matrice projection au vertex shader (uniform). Tout d’abord nous utilisons la classe Matrix4 offerte par le squelette pour calculer la matrice de projection. Dans le constructeur GLApplication::GLApplication :
    // _projection de classe Matrix4 (déclaré dans GLApplication.h) _projection.setOrtho(-20,20,-20,20,5,100); // cf calcul de la matrice dans le cours
  3. Il reste à passer cette matrice au vertex shader. Dans GLApplication::draw :
    _shader.uniform("projection",_projection); // utilisation de la classe shader // qui permet d'alléger la syntaxe OpenGL
  4. Testez (il s’agit toujours du tétraèdre avec élimination des parties cachées et des coordonnées différentes).

Question 12. Comprenez bien l’intérêt : on definit les données 3D (le tétraèdre ici) par rapport à un repère plus "intuitif" (l’observateur) et « usuel»(le repère est direct : z négatif = devant l’observateur). Le volume de visualisation (i.e. l’écran et le champ de vision) est alors défini par rapport à cet observateur de manière souple et libre.

Question 13. Testez et comprenez :

  1. _projection.setOrtho(-18,22,-10,30,5,100);
  2. _projection.setOrtho(-5,2,-10,10,5,100);

Comprenez l’interprétation de ces modifications par rapport au résultat : on ne « bouge»pas les points de la "scène", mais seulement "l’écran" (i.e. le volume de visualisation) par rapport à l’observateur.

Question 14. Il peut être tentant de se débarasser de la contrainte du far pour visualiser les objets devant l’observateur quelque soit leur distance (un paysage étendu, une ville, etc) en mettant une valeur très grande : testez _projection.setOrtho(-20,20,-20,20,5,1e8); (remarque : le résultat peut dépendre du matériel selon que la valeur du depth buffer est stockée sur 16 ou 24 bits : mettez éventuellement une valeur encore plus grande pour far pour constater un effet similaire à l’image suivante).

Pour expliquer ce résultat, comprenez que les valeurs de profondeur sont converties dans [0,1] (Window Coordinates) avec une certaine précision : ici l’imprécision des valeurs obtenues met en défaut le depth buffer (les valeurs de profondeur deviennent égales même pour des points relativement éloignés les uns des autres).

Revenez à un far raisonnable (100 par exemple).

4  Un peu d’animation

Question 15. Outre le calcul de la projection, on peut appliquer à la position passée au vertex shader n’importe quelle transformation. Nous allons tourner le tétraèdre en appliquant une matrice _transform dans le vertex shader (cette matrice est déjà définie et calculée par le squelette pour qu’elle corresponde à une matrice de rotation; nous verrons les détails sur les transformations en cours; l’angle de rotation évolue dans GLApplication::update pour obtenir une rotation animée). Modifiez alors le vertex shader pour intégrer :

... eyePosition=transform*eyePosition; // application de la rotation clipPosition=projection*eyePosition; // puis application de la matrice de projection ... // ou bien plus directement clipPosition = projection*transform*eyePosition;

Affectez le uniform correspondant (comme pour la projection) dans GLApplication::draw. Testez : le tétraèdre doit tourner (vous pouvez régler la vitesse en changeant le pas de l’angle dans GLApplication::update). Nous généraliserons ce principe avec n’importe quelle transformation lors du cours sur les changements de repères.

5  Objets plus complexes

On peut, bien sûr, construire des objets plus complexes que le tétraèdre sur le même principe : par exemple, pour un cube, avec une couleur par face, combien faudrait-il de sommets ? On peut également générer, algorithmiquement un cylindre, une sphère, un tore, etc. On peut également lire les données depuis un fichier (fichier issu d’un logiciel de modélisation par exemple).

Question 16. Ouvrez cube.obj avec un éditeur de texte : la position des sommets est donnée par ’v’ (8 sommets pour un cube). Les normales (i.e. les vecteurs orthogonaux à l’objet : nous verrons leur utilité dans la partie "éclairement" du cours) sont donnés par vn. Nous interpréterons pour l’instant ces normales comme des valeurs de couleurs. Puis sont décrites les faces de l’objet par ’f’ (il y a bien 6 faces pour un cube). Les entiers suivants ’f’ sont les indices des positions ’v’ et ’vn’ (le premier ’v’ du fichier correspond à l’indice 1). Par exemple :

f 1//1 2//1 3//1 4//1

est le polygone constitué des positions (1,2,3,4) (1 = premier ’v’ dans le fichier, 2 = deuxième ’v’, etc), et des normales pour chaque sommet (ici la même normale numéro 1 pour chaque sommet). Autrement dit chaque sommet de la face est spécifié par numéro v//numéro vn.

Constatez que ces données nécessitent un traitement pour correspondre aux contraintes de tracé d’OpenGL :

  1. Les faces dans le fichier ne sont pas nécessairement des triangles (il faut donc les transformer en triangles pour les tracer avec OpenGL : OpenGL ne sait tracer que des triangles)
  2. Les attributs position/normale pour chaque sommet sont sans redondance (i.e. une même position peut être associée à différentes normales alors que pour OpenGL il s’agit nécessairement de 2 sommets distincts).

Une classe du squelette ObjLoader (dans le répertoire p3d/scene) permet de lire ces fichiers .obj en effectuant les traitements nécessaires : triangulation et création des sommets (1 sommet = tous ses attributs).

Pour lire un .obj, dans le squelette, il suffit de faire (dans le constructeur GLApplication::GLApplication) :

// _obj déjà déclaré en ObjLoader _obj.readInit("cube.obj",Vector3(-10,-10,-30),Vector3(10,10,-10)); // reporte l'objet dans la boite d'extémités (-10,-10,-30) et (10,10,-10) _basicMesh.initObj(_obj); // pour remplacer l'initialisation du tétraèdre

Il reste à compléter BasicMesh::initObj dans la question qui suit.

Question 17. Dans BasicMesh::initObj il s’agit d’affecter _attribute (comme dans initTetrahedron) mais en prenant les données dans obj (voir les commentaires pour la description des accesseurs nécessaires).

obj contient la liste de tous les triangles et pour chaque triangle, on connait tous les attributs de chaque sommet : pour affecter _attribute on parcourt donc simplement tous les sommets de tous les triangles. Il n’y a donc plus besoin de la notion d’Indexed Face Set et _element devient donc inutile (on tracera donc en glDrawArrays et non en glDrawElements).

Complétez BasicMesh::initObj, et passez en glDrawArrays dans BasicMesh::draw : vous devez voir le cube tourner (remarque : les normales sont interprétées comme des valeurs de couleurs, puisqu’on n’a rien changé au niveau du vertex/fragment shader; des faces apparaissent donc noires car les normales peuvent avoir des coordonnées négatives : pour une composante de couleur, toute valeur négative est interprétée comme la valeur 0).

Question 18. Les normales ont des coordonnées dans [−1,1]. Comme on les interprète comme des couleurs, on va les reporter dans [0,1]. Reportez les coordonnées des normales dans [0,1] (i.e. -1 doit correspondre à 0 et 1 à 1) dans BasicMesh::initObj.

Question 19. Changez cube.obj par cow.obj. Testez

Question 20. Il s’agit dans cette question de réfléchir sur les Indexed Face Set (i.e. faire un glDrawArrays ou un glDrawElements ?) :

Il s’agit d’un excellent exercice de faire une version de loader permettant d’exploiter l’indexed face set en OpenGL (hors sujet du tp). En pratique, dans les librairies haut niveau, on retrouve les 2 approches; le critère de choix s’appuie entre optimisation mémoire OpenGL et complexité de la représentation/manipulation des données. Il est cependant souvent admis qu’il ne faut pas chercher à optimiser la mémoire des tracés en OpenGL (faire du glDrawArrays même s’il y a de nombreuses redondances) : le gain apparent risque d’être perdu sur le traitement des données pour alimenter OpenGL (préparation des données effectués par le CPU)...

Question 21. Avant de passer à l’exercice suivant immobilisez la vache en forçant _angle=0.0 dans GLApplication::update (nous ne tiendrons pas compte des transformations pour le calcul d’éclairement). Si vous oubliez de désactiver la rotation de la vache votre résultat sera faussé.

6  Eclairement

Question 22. Nous allons interpréter les normales en tant que telles pour faire un calcul d’éclairement. Tout d’abord, pour que le code soit plus lisible, changez le nom du in vec3 color en in vec3 normal (pensez à modifier le _shader.attribute("normal",1) en conséquence dans GLApplication::initialize). Testez pour d’éventuelles erreurs (ne doit rien changer au résultat obtenu). Enlevez également le report dans [0,1] des valeurs dans BasicMesh::initObj (i.e. faire directement _attribute.push_back(obj.normal(i,j).x());, etc).

Question 23. On rappelle que l’intensité d’éclairement (un nombre compris entre 0 et 1) diffus est donné par N · LN est la normale au point à éclairer, et L est la direction d’éclairement (cf cours). Nous allons calculer l’intensité d’éclairement en chaque sommet dans le vertex shader. La normale N est déjà donnée (c’est l’attribut normal). La direction d’éclairement correspond au vecteur PSP est la position du sommet (attribut position) et S est la position de la source lumineuse. Réalisez l’éclairement des sommets avec les étapes suivantes :

  1. Un membre Vector3 _lightPosition est déclaré dans le squelette pour représenter la position de la source. La position de la source sera interprétée dans le repère Eye : fixez la pour l’instant à Vector3(0,0,0) (i.e. sur l’observateur). Il faut passer la position de cette source au shader par uniform. Faites-le (un uniform lightPosition dans le vertex shader et affectation par _shader.uniform("lightPosition",lightPosition).
  2. Dans le vertex shader définissez, localement au void main(), un vec3 N (pour la normale) et un vec3 L (pour la direction d’éclairement) et affectez les (remarque : d’après le calcul vu en cours, il faut assurer que les vecteurs N et L soient de norme 1 : vous pouvez directement faire N=normalize(N); en GLSL).
  3. Il reste à calculer float intensity = max(dot(N,L),0.0) (le max force l’intensité à 0 si le produit scalaire est négatif).
  4. Enfin attribuez à fColor la couleur vec3(intensity,intensity,intensity) (donnera une couleur en nuance de gris selon la valeur de intensity).

Testez !

Question 24. Testez avec une autre position pour la source lumineuse (constatez la différence sur les ombrages).

Question 25. Ajoutez un uniform au vertex shader pour paramétrer la couleur de l’objet (i.e. fColor=intensity*diffuseColor). Remarque : vous pouvez directement affecter un uniform de la manière suivante : _shader.uniform("diffuseColor",Vector3(0.2,0.8,0.2)); (i.e. sans définir nécessairement une variable dans l’application). Ce paramètre diffuseColor correspond au coefficient de réflexion diffuse (spécifie la couleur mat d’un matériel).

Question 26. Bonus. Réactivez l’animation (i.e. _angle+=2). Si la visualisation ne semble pas correcte, vérifiez que vous avez appliqué l’éclairement avec les positions et les normales transformées par la matrice transform.


Ce document a été traduit de LATEX par HEVEA