Introduction
Objectif général
L'objectif de la suite des séances est de partir d'une scène vide, et d'y ajouter au fur et à mesure des séances différents éléments afin de construire une scène 3D "complexe".
- Séance modélisation (I)
-
- Création d'un terrain et ajout d'arbres
- Séance texture (II)
-
- Ajout de texture et de billboard sur le terrain. Ajout de déformation procédurale sur le terrain.
- Séance animation par modèle générateur (III)
-
- Ajout d'un modèle d'oiseau suivant une trajectoire dans la scène.
- Séance animation par modèle physique (IV)
-
- Ajout d'une corde.
Pour vous familiariser avec les notions de textures et d'animations en dehors de votre scène "principales", d'autres scènes 3D annexes minimalistes vous seront proposées.
Rem. Les deux dernières scènes finales sont ambitieuses, il ne sera pas indispensable d'arriver à incorporer l'ensemble de ces éléments dans votre scène au bout des 5 séances.
Bibliothèque de code

Dans la suite des TD, vous disposez d'une bibliothèque de code (appelée VCL: Visual Computing Library) vous permettant de développer des applications en OpenGL sans avoir à tout recoder à partir de zéro.
La bibliothèque propose un ensemble de fonctionalité basique pour faciliter la programmation 3D tels que
- Des vecteurs et matrices (2D, 3D, 4D) - en partie similaires à celles disponible en GLSL
- Des structures de maillages prêtent à être affichées avec OpenGL, ainsi que les shaders associées
- Des controleurs de scène 3D tel qu'une caméra controlable à la souris.
L'objectif de cette bibliothèque est d'être aussi légère que possible afin de vous permettre de développer plus aisément, tout en restant très proche du
code OpenGL basique (sans états cachées).
Bien que cela ne soit pas nécessaire, vous devriez être en mesure de comprendre l'ensemble des parties du code, qui est développé suivant les principes
suivants
- - Code écrit de manière minimaliste, afin d'être simple à comprendre (pas nécessairement le plus générique ou efficace).
- - Objets écrits pour être légers et sans états cachés. Vous pouvez généralement modifier directement les attributs des objets.
- - La bibliothèque n'impose pas de "framework" de rendu, ainsi le code et les fonctionalitées restent proche d'un développement C++/OpenGL à partir de rien, et il est possible de réaliser des appels directs à OpenGL sans l'aide de la bibliothèque.
Rem. La bibliothèque est développée pour vos TD, il ne s'agit pas d'une bibliothèque standard.
Téléchargement et lancement de la bibliothèque
La bibliothèque de code est hébergée sur github: https://github.com/drohmer/inf443-vcl
- Remarque: Ce code pourra être mise à jour au cours des séances si cela se révèle nécessaire
> Téléchargez, compilez et exécutez le code présent dans le répertoire scene/inf443/00_introduction/. Vous devriez voir apparaitre une scène 3D avec un quadrangle.
- Notez que dans ce cas, le "répertoire racine" pour la compilation est scene/inf443/00_introduction/
- Il faut donc creer votre projet depuis le fichier scene/inf443/00_introduction/CMakeLists.txt
- Et executer votre executable depuis ce répertoire scene/inf443/00_introduction/
Lorsque le programme est lancé un modèle de caméra manipulable à la souris/clavier est déjà implémenté. Il permet les transformations suivantes
- Rotation: clic gauche + déplacement de la souris (modèle de trackball par défaut)
- Zoom: clic droit + déplacement haut/bas de la souris
- Panning (/translation dans le plan de la caméra): CTRL + clic gauche + déplacement de souris
- Avance/Recule (/translation dans le plan orthogonal à la caméra): CTRL + clic droit + déplacement haut/bas de la souris
Structure générale du code
Role des principaux répertoires
- scenes/ Contient les codes associées à la mise en place des scènes 3D.
-
- - Les exercices correspondent au code décrit dans le répertoire scenes/inf443/. Vous éditerez le code principalement dans ces répertoires.
- - Chaque répertoire contient un code executable différent. Pour passer d'un exercise à l'autre, il est nécessaire de compiler le code correspondant.
- library/vcl/ Contient le code source de la bibliothèque VCL: ensemble de structures et fonctions pour facilier la mise en place de scène 3D.
- library/third_party/ Bibliothèques externes utilisées par VCL tel que glad (OpenGL loader), imgui (GUI), lodepng (chargeur d'images PNG).
Bibliothèque VCL
Le code source de la bibliothèque VCL est organisé de la manière suivante- vcl/base/ fonctionalités de bases de la bibliothèque tel que les assertions et gestions d'erreurs.
- vcl/containers/ définit des conteneurs basiques avec des fonctionalités permettant de faciliter l'utilisation de buffer de données: ex. buffer = version étendue d'un std::vector, et buffer_stack = version étendue d'un std::array.
- vcl/math/ définit des fonctions d'aide mathématiques et des structures de vecteurs et matrices 2/3/4D.
- vcl/shape/ correspond aux structures et fonctions relatives aux objets 3D tels que les maillages et courbes, indépendamment de leur affichage.
- vcl/display/ contient les objets et fonctions facilitant l’utilisation l'affichage: appels OpenGL, ou structures "_drawables". Les structure "_drawables" correspond à la gestion des elements stockées sur le GPU (VBOs/VAO) et paramètres uniformes pour différent type de formes.
- vcl/interaction/ contient les objets et fonctions non géométriques associées à l'interaction avec la scène 3D tel que la caméra ou les "timers".
- vcl/shaders_preset/ shaders pré-codés tels que l'illumination de phong pour des maillages.
Rem. Pour les étudiants sous Linux/MacOS: La librairie de code contient de nombreuses fonctions et structures réparties dans plusieurs fichiers. Pour pouvoir utiliser efficacement cette librairie, il est important que votre éditeur puisse: auto-compléter vos noms de variables/functions, vous indiquer les arguments attendues par les fonctions, et vous permettre de naviguer aisément vers les définition des fonctions et structures.
Pour cela, il est fortement conseillé que vous testiez l'utilisateur d'IDE (tels que QtCreator par ex.) dans le cas où vous éditiez votre code jusqu'à présent avec des éditeurs de texte légers (Visual Studio Code, etc.).
Utilisation de la bibliothèque
- Observez le fichier main.cpp et trouvez la boucle d'affichage. Notez la présence des fonctions: initialize_data() qui est uniquement appelée une unique fois en début de programme, et display_scene() appelée en permanence dans la boucle d'affichage.
- Pour l'instant 3 formes sont affichées: un repère (frame_visual), une sphère et un plan (représenté par un quadrangle).
- Notez que la demande d'affichage de ces formes est réalisée par l'appel à la fonction "draw".
-
- - Cette fonction nécessite une structure de type mesh_drawable et les éléments de la scène (caméra, position de la lumière, matrice de projection, etc).
- - Par défault, la fonction "draw" utilise le shader définit préalablement par "mesh_drawable::default_shader" - ici une illumination de type phong. (mais il serait possible d'associer un shader propre à chaque objet au besoin).
- - Si vous observez le code de la fonction "draw(mesh_drawable, SCENE)", vous noterez que cette fonction envoie les paramètres uniformes au GPU avant de faire l'appel à glDrawElements.
-
-
- Rem. avancé: Le paramètre scene est en fait un type générique (dit template) qui permet d'utiliser cette fonction même si vous modéliser votre environement de manière différente.
-
Affichage "wireframe"
Notez l'appel à la fonction draw_wireframe permettant d'afficher la triangulation d'une forme à l'aide d'une couleur unique.
N'hésitez pas à utiliser cette fonction afin d'aider au debug pour mieux visualiser l'organisation des triangles des formes que vous allez pouvoir créer par la suite.
> Affichez la triangulation de la sphère par la même approche, et notez que vous pouvez adapter la couleur (r,g,b)\(\in [0,1]^3\) utilisée pour l'affichage des segments.
Ajout d'une primitive
Ajoutez l'affichage d'un cube dans la scène. Pour cela, on procèdera de la manière suivante
- 1. Ajoutez la déclaraction d'une structure mesh_drawable cube; dans les variables globales.
- 2. Ajoutez le code suivant: création d'une sphère, ainsi que sa conversion en mesh_drawable dans la fonction initialize_data
mesh cube_cpu = mesh_primitive_cube(); // generate the triangulation of the cube cube = mesh_drawable(cube_cpu); // send the mesh structure to the GPU cube.shading.color = {0,1,0}; // set the color of the cube to be green cube.transform.translate = {-1,-1,1}; // translate the cube
- 3. Ajoutez l'appel à l'affichage du cube dans frame_draw
draw(cube, scene);
Rem. La fonction mesh_primitive_cube peut prendre deux arguments optionnels: le centre du cube, et la longueur des arêtes.
Rem. Structures mesh/mesh_drawable
Notez la présence des deux types de variables: mesh et mesh_drawable.
- Structure mesh
La structure mesh correspond à la structure d'une surface triangulée stockée en mémoire centrale (RAM) par des buffers de données accessibles depuis le code C++.
struct mesh { buffer<vec3> position; buffer<vec3> normal; buffer<vec4> color; buffer<vec2> uv; buffer<uint3> connectivity; ... }
Cette structure est utile lors de la création d'une forme, ou lorsque les coordonnées doivent être modifiés interactivement. On peut alors accéder à n'importe quelle données dans une fonction (ex. cube_cpu.position[2] += {0,-1,0};)
Ces données ne sont par contre pas directement accessible à la carte graphique pour réaliser l'affichage de la forme correspondante. On peut donc manipuler les données de cette structure, mais pas l'afficher directement
- Structure mesh_drawable
A l'inverse, la structure mesh_drawable est l'équivalent d'une structure de type mesh mais lorsque les données sont sur la mémoire de
la carte graphique. Les données (position, normal, etc) ne sont alors plus accessible directement dans le code C++ et seuls les indices des VBO et du VAO
sont accessibles.
De plus cette structure stocke des informations supplémentaires tels que le shader et l'indice de texture potentiellement utilisés, ainsi que les variables
uniformes associées à la forme qu'il faut envoyer à chaque affichage (transformation affine, shading/couleur).
Cette structure est pensée pour être prette à être affichée lors de l'utilisation de la fonction draw.
struct mesh_drawable { std::map<std::string, GLuint> vbo; // Generic storage for a set of VBOs GLuint vao; GLuint number_triangles; GLuint shader; GLuint texture; // Uniform affine_rts transform; shading_parameters_phong shading; }
- Conversion mesh vers mesh_drawbable
Une structure de type mesh peut être convertie en mesh_drawable par l'appel au constructeur mesh_drawbable(variableMesh). Lors de cette construction, les appels à glBufferData sont réalisés sur les différents champs de la variable mesh.
Notez que la variable de type mesh, et mesh_drawable sont indépendantes. Une fois mesh_drawable générée les modifications apportées à la variable mesh n'y sont pas reportée. Il ne faut donc générer la variable de type mesh_drawable qu'une fois que votre structure mesh est finalisée.
Lorsque vous afficher un maillage qui ne varie pas, la structure mesh n'est qu'une étape temporaire avant sa conversion vers une structure de
type mesh_drawable servant à son affichage. Il est donc possible de créer la variable de type mesh comme une simple variable locale qui a
une durée de vie limitée.
Il est également possible de se passer totalement du stockage explicite de la variable de type mesh, et d'enchainer sa création avec la conversion
vers un mesh_drawable: Par exemple, il serait possible possible d'écrire cube = mesh_drawable(mesh_primitive_cube());
Notez cependant qu'il n'est pas prévue de récupérer en RAM des informations passée sur le GPU. C'est à dire qu'une structure de type mesh_drawable ne peut pas être convertie en mesh. S'il est nécessaire de modifier des éléments de la structure mesh, il faudra stocker cette variable.
- Synthèse
-
- Les structures de type mesh sont utilisés lors de la création des maillages, ou pour avoir accès aux sommets de manière individuels. Ces
structures stockent les éléments en mémoire RAM et ne sont pas directement affichables.
- Les structures de type mesh sont utilisés lors de la création des maillages, ou pour avoir accès aux sommets de manière individuels. Ces
structures stockent les éléments en mémoire RAM et ne sont pas directement affichables.
-
- Une fois complétées, les structures de type mesh peuvent être converties en mesh_drawable qui contiennent les informations des
VBO/VAO, ainsi que shaders, textures, et variables uniformes permettant l'affichage.
- Une fois complétées, les structures de type mesh peuvent être converties en mesh_drawable qui contiennent les informations des
VBO/VAO, ainsi que shaders, textures, et variables uniformes permettant l'affichage.
-
- Les mesh_drawable ne peuvent être manipulés efficacement que par l'intérmédiaire des variables uniformes (et donc par le biais des shaders).
Affichage de multiples instances d'un même objet
Supposons que l'on souhaite afficher plusieurs cubes ayant des propriété différentes (couleur, position, etc).
Il est possible de réutiliser une même structure mesh_drawable, mais d'afficher celle-ci à plusieurs reprise en modifiant ces paramètres uniforms avant chaque affichage.
Remplacez la ligne draw(cube, scene); par le code suivant
cube.shading.color = {1,1,0}; cube.transform.translate = {2,2,0.5}; draw(cube, scene); cube.shading.color = {1,0,1}; cube.transform.translate = {-1,-2,0.5}; draw(cube, scene);
Observez le résultat qui affiche désormais deux cubes. Le premier, jaune, est situé à la position \((2,2,0.5)\), alors que le second, magenta, est situé en position \((-1,-2,0.5)\). Notez que les paramètres uniforms doivent être ajustés avant (et non après) la demande d'affichage de l'objet.
Cette approche est efficace pour afficher un grand nombre d'objets en étant économe en place mémoire: ici un seul maillage générique a besoin d'être stocké, mais il est affiché à plusieurs reprises avec des paramètres uniformes différents.
Affichage d'une surface
Dans cette partie, nous allons modéliser une surface sous la forme d'une fonction de hauteur \(z=f(x,y)\).
Considérez la fonction suivante
mesh create_terrain() { // Number of samples of the terrain is N x N const unsigned int N = 100; mesh terrain_mesh; // temporary terrain storage (CPU only) terrain_mesh.position.resize(N*N); // Fill terrain geometry for(unsigned int ku=0; ku<N; ++ku) { for(unsigned int kv=0; kv<N; ++kv) { // Compute local parametric coordinates (u,v) \in [0,1] const float u = ku/(N-1.0f); const float v = kv/(N-1.0f); // Compute the local surface function const float x = 5*(u-0.5f); const float y = 5*(v-0.5f); const float z = std::exp(-(x*x+y*y)); // Store vertex coordinates terrain_mesh.position[kv+N*ku] = {x,y,z}; } } // Generate triangle organization // Parametric surface with uniform grid sampling: generate 2 triangles for each grid cell for(size_t ku=0; ku<N-1; ++ku) { for(size_t kv=0; kv<N-1; ++kv) { const unsigned int idx = kv + N*ku; // current vertex offset const uint3 triangle_1 = {idx, idx+1+N, idx+1}; const uint3 triangle_2 = {idx, idx+N, idx+1+N}; terrain_mesh.connectivity.push_back(triangle_1); terrain_mesh.connectivity.push_back(triangle_2); } } // update the other buffers (normal, color, uv) with default parameters terrain_mesh.fill_empty_field(); return terrain_mesh; }
- Cette fonction génère une structure de type mesh dont les sommets sont définis par un échantillonnage régulier sur une grille le long des axes \((x,y)\), et dont la hauteur est définie par \(z=exp(-(x^2+y^2))\).
- Utilisez cette fonction pour générer et afficher la surface associée (vous devez créer une nouvelle variable de type mesh_drawable).
- Affichez le mode wireframe de ce maillage pour observer la répartition des triangles.
- Modifiez la fonction de hauteur et observez le résultat.