Séance 1: Scène 3D
Scène basique et controle
Compilez et exécutez le code situé dans le répertoire scenes_csc43043ep/01_introduction- Vous devez suivre la même démarche expliquée dans le tutoriel de compilation dans le cas de ce répertoire.
- - un repère dont les axes rouge-vert-bleu indiquent respectivement la direction \((x,y,z)\) dans le repère du monde. Ce repère vous sera utile visuellement pour placer vos objets, et il peut être activé/desactivé dans cet exemple en cliquant dans l'interface sur le bouton "Frame".
- - un cube et un plan texturé représentant deux objets 3D visibles dans cette scène.
- - Rotation: clic gauche suivi d'un déplacement de la souris.
- - Avance/Recule: clic droit suivi d'un déplacement de la souris haut/bas. La caméra se rapproche/s'éloigne de son centre de rotation.
- - Panning (= translation dans le plan de la caméra): CTRL + clic gauche + déplacement de la souris
- - Déplacement avant/arrière: CTRL + clic droit + déplacement haut/bas de la souris. La caméra ainsi que son centre de rotation se déplacent.
Code
Le code correspondant à cette scène se décompose en deux parties:- 1- Les fichiers situés dans scenes_csc43043ep/01_introduction/, et en particulier le fichier scenes.cpp contient la mise en place spécifique de la scène 3D. Vous allez principalement éditer le code C++ dans ce fichier. On peut également noter le fichier main.cpp qui contient le setup générique d'une scène que qui sera similaire pour l'ensemble des exercices.
- 2- La bibliothèque de code (CGP) que l'on va utiliser dans le cadre de ses sessions pratiques afin de vous aider dans la programmation graphique (structure de vecteurs, matrices, maillage, affichage, camera, etc), mais restant très simple et bas-niveau (proche d'OpenGL). Cette bibliothèque se situe dans le répertoire cgp/. Vous n'aurez généralement pas besoin de modifier ces fichiers.
- - la compréhension du langage C++ qui est généralement nouveau pour vous.
- - l'utilisation d'une bibliothèque de code (CGP).
- - la logique de fonctionnement d'OpenGL et des shaders.
- - l'algorithmique et la compréhension liée à la partie graphique.
Ajout d'un élément dans la scène
Objectif: Nous allons dans un premier temps ajouter une sphère dans cette scène.Préambule
Affichage en ligne de commande: cout
A chaque exécution du programme, des informations s'affichent sur la ligne de commande. Sous Windows, il s'agit d'une fenêtre annexe textuelle qui peut parfois apparaitre derrière la fenêtre de l'affichage de la scène. Gardez en tête qu'il est important de garder régulièrement un oeil sur cette ligne de commande car il le programme y affiche des informations utiles de debug/warnings. Si votre programme crash, il s'agit de la première vérification à réaliser. La commande C++ "std::cout<<" ... "<<std::endl;"permet l'écriture de l'état des variables sur la ligne de commande (similaire à print() pour Python ou System.out.println() pour Java)-
- - std:: signifie l'appel à une fonction de la bibliothèque standard du C++
- - cout signifie Common Output.
- - endl signifie end of line.
- - << est un opérateur en C++ qui est utilisé ici pour concaténer des chaine de charactères à afficher.
Commentaires
- - En C++, les commentaires peuvent se déclarer suivant deux manières:
// Ceci est un commentaire qui s'arrête au bout de la ligne /* Ceci est un commentaire qui perdure ... ... jusqu'à rencontrer le symbole suivant */
Fichier scene.cpp
Le programme contient deux étapes principales que vous trouvez dans le fichier scene.cpp:- 1. Une étape d'initialisation des données par l'appel à la fonction scene_structure::initialize(). Cette fonction est appelée une unique fois en début de programme.
-
- L'objectif de cette fonction est de réaliser l'ensemble des pré-calculs qui réalisent les opérations couteuses en temps (allocations mémoires, initialisation des coordonnées des formes, etc.).
- 2. Une boucle d'animation qui tourne en permanence et qui appelle la fonction scene_structure::display_frame() (et scene_structure::display_gui) à chaque frame.
-
- L'objectif de la fonction display_frame() est de réaliser les appels à l'affichage des éléments de la scène par le GPU. Cette fonction étant (ré)-appelée en permanence, elle ne doit contenir que des appels légers en temps de calculs.
Dans le fichier "scene.cpp", le nom des fonctions sont précédés de "scene_structure::". Cette syntaxe indique qu'il s'agit de fonctions qui sont membres de la classe "scene_structure" - en orienté objet, ont dit que ce sont des "méhodes" de la classe. Les méthodes de la classe ont accès aux variables partagées par une même instance.
La définition de la class "scene_structure" est visible dans le fichier dit "d'en tête"/header scene.hpp.
Pour ajouter un nouvel objet 3D, nous allons désormais suivre le même processus que pour les variables "cube" et "ground" que vous pouvez déjà voir dans le code.
> Dans la fonction initialize() (on omet de mentionner scene_structure:: lorsqu'il n'y a pas d'ambiguité), créez une variable sphere_mesh qui va contenir la structure du maillage de la sphère.
mesh sphere_mesh = mesh_primitive_sphere(1.0f);
- - La fonction mesh_primitive_sphere est une fonction précodée de la bibliothèque CGP qui initialise un maillage dont les sommets sont positionnés sur une sphère dont le rayon est passé en paramètre (par défault, le rayon est 1). Les coordonnées sont calculées à partir de leurs coordonnées sphériques.
- - Cette fonction renvoie une structure de type "mesh". Cette structure stocke la liste des informations par sommets (coordonnées, normales, couleur, uv). Ces données sont stockées en mémoire RAM et sont pratiques pour être modifiées dans le code. Par contre elles ne peuvent pas être directement affichées. Pour cela, il est nécessaire d'envoyer ces données préalablement sur la carte graphique.
Ajout de la sphère
Le process de passage des données du CPU vers la carte graphique (GPU) est géré par la structure mesh_drawable (structure prévue dans la bibliothèque CGP). > Pour cela, suivez la démarche suivante:- a. Créez une variable de la classe scene_structure (/un attribut) de type mesh_drawable. Les variables de la classe scene_structure se définissent dans le fichier d'en-tête scene.hpp, à l'intérieur de la définition de la classe.
// dans scene.hpp struct scene_structure { ... // Ajoutez votre variable de classe // (par exemple après la ligne "mesh_drawable cube;") mesh_drawable sphere; ... };
- b. Initialisez le contenu de sphere à partir de sphere_mesh dans la fonction initialize() (dans le fichier scene.cpp)
// Ecrire à la suite de "mesh sphere_mesh = mesh_primitive_sphere();" sphere.initialize_data_on_gpu(sphere_mesh);
- c. Demandez l'affichage de la sphere dans la fonction display_frame() (par exemple après "draw(cube, environment);")
draw(sphere, environment);
Explication
- - L'étape a. consiste à déclarer une variable de la classe scene_structure avec son type. La variable n'est pas initialisée à ce stade.
-
- - La variable sphere_mesh n'est utilisée que dans la fonction initialize(). Elle est supprimée de la mémoire après la fin de cette fonction, et n'est pas utilisé par la suite lorsque l'affichage commence. On peut donc déclarer cette variable "localement" dans la fonction initialize().
- - Au contraire, la variable sphere est utilisée dans deux fonctions: initialize(), et display_frame(). Cette variable doit être partagée entre ces deux fonctions, et exister tout au long de l'affichage et des appels régulier à display_frame plusieurs fois par secondes. Pour cela, on doit déclarer sphere comme une variable de la classe (dans le fichier d'en-tête scene.hpp).
- - Une approche alternative serait de déclairer sphere_mesh comme une variable "globale": en haut du fichier scene.cpp en dehors de toute fonction. Il est cependant préférable d'utiliser une variable de classe lorsque c'est possible pour structurer les programmes.
- - L'étape b. permet d'envoyer les données contenues dans la classe mesh sur la carte graphique (initialize_data_on_gpu). Cette étape ne doit être réalisée qu'une seule fois. La structure mesh_drawable est prévue par la bibliothèque CGP - cette structure réalise des appels OpenGL directs que nous expliquerons dans des séances ultérieurs.
- - L'étape c. correspond à la demande d'affichage des données, ainsi que l'envoi de paramètres spécifiques (translation, position de la caméra, lumière, etc) aux programmes exécutés sur la carte graphique qui sont appelés shaders. Nous verrons ces programmes dans la suite. Cette demande d'affichage doit nécessairement être réalisée dans la boucle d'affichage
- \(\Rightarrow\) Notez qu'à chaque instant tous les objets de la scène sont ré-affichés dans leur ensemble (typiquement 60 fois par secondes). Cela permet l'impression d'animation et d'interaction lorsque la caméra évolue. Il n'est pas possible d'afficher un objet dans la fonction initialize().
Modification de la sphère
Il est possible d'adapter des paramètres de l'objet tels que sa couleur, position, dimension en modifiant certaines variables/attributs de la classe mesh_drawable.Par exemple écrivez dans la fonction initialize() après sphere.initialize_data_on_gpu(...):
// to add after "sphere.initialize(sphere_mesh, "Sphere");" sphere.model.scaling = 0.2f; // coordinates are multiplied by 0.2 in the shader sphere.model.translation = { 1,2,0 }; // coordinates are offseted by {1,2,0} in the shader sphere.material.color = { 1,0.5f,0.5f }; // sphere will appear red (r,g,b components in [0,1])

- Notez "f" après les nombres à virgules (0.2f, 0.5f).
-
- - En C++, les valeurs à virgules (ex. 0.2) sont par défaut des nombres flottants dits à double précision: type "double" - encodés sur 8 octets.
- - Les cartes graphiques utilisent cependant des nombres flottants à simples précisions: type "float": encodés sur 4 octets. OpenGL et la bibliothèque CGP utilisent ainsi des types "float" par défaut et non pas des "double".
- - Dans la grande majorité des cas, vous pouvez écrire dans le code "0.2" à la place de "0.2f" sans problèmes: le compilateur convertira de lui-même la valeur à double précision vers simple précision. Dans certains cas particuliers, le compilateur pourrait cependant indiquer un warning (perte de précision), voir une erreur (mélange de type entre float et double en paramètre templates), qui nécessiterait d'expliciter le type flottant simple précision.
- - Les codes d'exemples expliciteront généralement l'utilisation des flottants à simple précision avec la lettre "f".
Chargement d'un maillage externe
La bibliothèque CGP fournit des fonctions de créations de primitives basiques (sphères, cube, cylinder, cone, etc) par le biais de l'appel "mesh_primitive_xxx". Notez que vous pouvez taper mesh_primitive_ ou cgp::mesh_primitive_ dans votre IDE qui (une fois bien configuré) devrait vous proposer la liste des possibilités.
Un IDE (ici Visual Studio) auto-complète et fourni automatiquement la liste des fonctions disponibles et paramètres attendus.
Pour cela, une fonction de chargement simple d'un format classique: OBJ est fourni par défaut. Suivez la démarche suivante pour charger l'exemple d'un modèle de dromadaire:
- - Créez la variable de classe suivante (dans scene.hpp):
mesh_drawable camel;
- - Chargez le maillage depuis le fichier, et initialisez la variable camel dans la fonction initialize().
// mesh_load_file_obj: lit un fichier .obj et renvoie une structure mesh lui correspondant mesh camel_mesh = mesh_load_file_obj(project::path + "assets/camel.obj"); // Initialisation classique de la structure mesh_drawable camel.initialize_data_on_gpu(camel_mesh); // Ajustement de la taille et position de la forme camel.model.scaling = 0.5f; camel.model.translation = { -1,1,0.5f };
- - Ajouter l'appel à l'affichage dans la fonction display_frame()
draw(camel, environment);

Affichage wireframe
Il est souvent utile de pouvoir visualiser les maillages en wireframe (mode "fil de fer") qui représente explicitement les arêtes des triangles afin de mieux comprendre la structure, et/ou pour du debug. La bibliothèque CGP propose la fonction draw_wireframe(mesh_drawable, environment) précodée à cet effet. Ajoutez les lignes suivantes dans la fonction display_frame() et observez le résultat.draw_wireframe(ground, environment); draw_wireframe(sphere, environment); draw_wireframe(cube, environment); draw_wireframe(camel, environment);

// affiche les arêtes en rouge draw_wireframe(camel, environment, {1,0,0});
Buffer de profondeur
Par défaut, la scène est rendue en utilisant le buffer de profondeur (Depth/Z-Buffer).Pour rappel, ce "buffer" est similaire à une image annexe (qui n'est pas affichée) stockant la profondeur la plus proche des pixel/fragment visibles. Ce buffer est utilisé pour savoir si le pixel d'un triangle en cours d'affichage est visible ou s'il est caché par un objet déjà affiché et plus proche de la caméra. L'utilisation du buffer de profondeur est activée par défaut, ce qui permet un affichage cohérent indépendamment de l'ordre d'affichage des objets dans le code.
> Désactivez l'utilisation du buffer de profondeur (plus précisément l'écriture dans ce buffer) en ajoutant la ligne suivante au début de la fonction display_frame()
glDisable(GL_DEPTH_TEST);
En désactivant le buffer de profondeur, chaque objet (et chaque triangle individuel) est affiché dans l'ordre de leurs appels, même s'il se situe "derrière" un autre spatialement. Les objets sont donc affichés dans l'ordre de leurs appels - et les derniers objets à être affichés apparaitrons donc toujours "devant" les autres. > Echangez l'ordre d'appel dans le code entre deux objets (exemple entre le sol et le cube) et observez le résultat. Remarque: La désactivation du buffer de profondeur peut être utile dans certains cas particuliers. Par exemple pour l'affichage d'objets semi-transparent - un exemple sera proposé dans la séance consacrée au textures. Mais l'ordre d'affichage des objets doit alors être considéré avec soin.
GUI: Interface utilisateur
Le code prévoit également la possibilité d'intégrer une GUI (Graphical User Interface) qui permet d'ajouter des boutons/sliders associés à des variables de votre programme. Dans notre cas, le code utilise une bibliothèque externe appelée ImGui (simple et légère d'utilisation, et s'intègre à un contexte OpenGL).Ajout d'un bouton
Considérons un exemple d'utilisation de cette bibliothèque pour ajouter un bouton permettant de sélectionner si il faut afficher ou non les maillages en mode wireframe.Le principe est le suivant:
- a. Créez une variable de classe booléenne qui va permettre de stocker s'il faut afficher ou non le maillage en wireframe dans la structure gui_parameters définie dans le fichier scene.hpp.
struct gui_parameters { bool display_frame = true; // Ajout de la nouvelle variable display_wireframe ici bool display_wireframe = false; };
Il serait tout à fait possible de définir la variable display_wireframe directement dans la classe scene_structure, mais la class gui_parameters est prévue spécialement pour stocker ce type de variable. Notez la présence de l'attribut "gui_parameters gui;" dans la définition de scene_structure. Il n'y a aucune différence sur le plan de de l'efficacité au moment de l'exécution, mais séparer les données liées à la GUI de celle de l'affichage permet généralement de gagner en lisibilité du code.
- b. Affichez le bouton (checkbox) à l'aide d'ImGui dans la fonction display_gui(), et associez l'action de ce bouton à la variable.
ImGui::Checkbox("Wireframe", &gui.display_wireframe);
- - La syntaxe "&gui_display_wireframe" -- ou plus généralement "&variable" -- consiste à considérer "l'adresse mémoire" de la variable plutôt que sa valeur. On parle de "pointeur" sur une variable.
- - L'utilisation de l'adresse de gui_display_wireframe dans ce cas, permet à la fonction "ImGui::Checkbox" de modifier la valeur de gui_display_wireframe. Ce paramètre est donc un paramètre d'entrée et de sortie à cette fonction.
- - La syntaxe "ImGui::" indique qu'il s'agit d'une fonction de la bibliothèque ImGui. On parle de "namespace".
- - ImGui est une bibliothèque qui travaille en "mode immédiat". C'est-à-dire qu'à chaque frame, le bouton est créé et la variable qui lui est associée peut être modifiée. Les boutons peuvent être modifiés dynamiquement sans avoir à pré-concevoir une architecture fixe. D'autres bibliothèques (telles que Qt par exemple) utiliseront un principe différent où l'interface devra être spécifiée préalablement.
- c. Dans la fonction display_frame() demandez désormais l'affichage des draw_wireframe uniquement dans le cas où la valeur de gui_display_wireframe est à "true".
if (gui.display_wireframe == true) { draw_wireframe(ground, environment); draw_wireframe(sphere, environment, {1,0,0}); draw_wireframe(cube, environment); draw_wireframe(camel, environment); }
- - Vous pouvez écrire également plus simplement la condition
if (gui.display_wireframe) { ... }
Ajout d'un slider
ImGui peut également gérer des sliders qui vous permet d'ajuster manuellement la valeur d'une variable dans un intervalle.Considérons le cas où vous souhaitez translater suivant l'axe x le modèle de dromadaire dans l'intervalle \([-2,2]\). Pour cela, ajouter simplement la ligne suivante dans display_gui():
ImGui::SliderFloat("camel-x", &camel.model.translation.x, -2.0f, 2.0f);
Perspective
La bibliothèque CGP propose des modèles de projections de caméra pré-programmés dans le cas de représentation standard de projection en perspective.Modèle
Le modèle perspectif est celui proposé dans le code par défaut.Ce modèle permet de convertir l'espace visible (un cône tronqué à base rectangulaire appelé "frustum") vers l'espace normalisé de représentation attendu par le GPU (appelé "normalized device coordinate") correspondant à un cube dans l'intervalle \([-1,1]\).

- - L'angle de vue \(\theta\) (field of view fov).
- - La plus petite distance à partir de laquelle un objet peut être vu \(z_{near}\) (\(\simeq\) distance de l'écran au centre optique).
- - La distance la plus grande où un objet peut être vu (base de la pyramide) \(z_{far}\)
- - Le rapport largeur/hauteur de la fenêtre (aspect ratio \(a=width/height\)).
\(\mathrm{P}=
\left(
\begin{array}{rrrr}
f_x & 0 & 0 & 0 \\
0 & f_y & 0 & 0 \\
0 & 0 & C & D \\
0 & 0 & -1 & 0 \\
\end{array}
\right)\),
avec
\(\left\{
\begin{array}{l}
f_y = 1/\tan(\theta/2) \\
f_x = f_y/a \\
L = z_{near}-z_{far} \\
C = (z_{far}+z_{near})/L \\
D = 2\,z_{far}\,z_{near}/L
\end{array}
\right.\)
On pourra noter que l'application de cette matrice (en coordonnées homogènes) à un point \((x,y,z,1)\) permet:
- - De mapper le point \((0,0,-z_{near})\) de l'espace au point \((0,0,-1)\) dans l'espace normalisé.
- - De mapper le point \((0,0,-z_{far})\) de l'espace au point \((0,0,+1)\) dans l'espace normalisé.
Perspective dans le code
La matrice \(P\) utilisée dans le code est accessible en appelant la fonction suivante: "camera_projection.matrix()".Il est possible d'afficher cette matrice sur la ligne de commande en écrivant (par exemple dans la fonction initialize())
std::cout << str_pretty(camera_projection.matrix()) << std::endl;
(str_pretty est une fonction de CGP permettant d'exporter une chaine de caractères pour laquelle une matrice sera typiquement affichée ligne par ligne plutôt qu'une suite contigue de valeur)
Dans la bibliothèque, la matrice en tant que telle n'est qu'une variable temporaire utilisée pour l'affichage OpenGL. La structure "camera_projection" stocke en fait les paramètres fov, \(z_{near}\), \(z_{far}\), etc, qui permet de générer cette matrice.
Il est possible par exemple de forcer un angle d'ouverture de \(90^{\circ}\) en écrivant dans la fonction initialize():
camera_projection.field_of_view = Pi / 2.0f;
- - L'angle d'ouverture est stocké en radians et non pas en degrés.
- - La variable "Pi" est pré-codée dans la bibliothèque CGP.
- - Notez qu'en augmentant l'angle d'ouverture, vous pouvez visualiser une plus grande part de l'espace pour une position de caméra donnée. Par contre, les longueurs (et donc les formes telles que les sphères, carrés, etc) se déforment sur les bords de la fenêtre (effet "fish-eye").
- - L'angle d'ouverture classique représentant une "vision humaine" attendue sur un écran standard est de \(60^{\circ}\) ou moins.
Shaders
Jusqu'à présent, nous avons manipulé des paramètres prévus dans la bibliothèque de code C++, et l'affichage est "pris en charge" par la fonction draw. Cette fonction draw(mesh_drawable, environment) fait en fait appel à OpenGL, qui est lui-même une interface permettant d'utiliser la carte graphique pour de l'affichage de scène 3D.L'intérêt de faire appel à OpenGL et à la carte graphique directement (plutôt que d'utiliser par exemple une fonction "plot" dans un langage plus simple tel que Python) est l'extrême efficacité et flexibilité de ce qui est affiché. L'affichage - et plus généralement l'ensemble des calculs et opérations - réalisé par la carte graphique est paramétré par des programmes que l'on appelle des shaders.
Les shaders en OpenGL sont écrits dans un langage appelé le GLSL (OpenGL Shading Language) qui est proche du C++, mais n'en est pas. Il s'agit d'un langage plus simple qui se concentre sur les opérations vecteurs/matrices de dimension 2, 3 et 4.
La librairie CGP est écrite pour suivre en grande partie la syntaxe du code glsl, ce qui signifie que votre code C++ sera très ressemblant au code GLSL des shaders. Nous allons utiliser principalement deux type de shaders:
- - Le vertex shader qui correspond à un programme exécuté sur chaque sommet de l'objet en cours d'affichage.
- - Le fragment shader qui correspond à un programme executé sur chaque pixel apparent (appelé un fragment) d'une primitive (généralement un triangle) en cours d'affichage.
Vertex shader
Le vertex shader utilisé dans le cas présent correspond au fichier shaders/mesh/mesh.vert.glsl. Vous pouvez ouvrir ce fichier avec un éditeur de texte classique (ex. Visual Studio Code) et la plupart des éditeurs proposent des modules de coloration syntaxiques (vous pouvez l'installer dans Visual Studio et Visual Studio Code).#version 330 core // Previous line is the standard header for OpenGL 3.3 // Inputs coming from VBOs layout (location = 0) in vec3 vertex_position; layout (location = 1) in vec3 vertex_normal; layout (location = 2) in vec3 vertex_color; layout (location = 3) in vec2 vertex_uv; ...
- - Le shader reçoit en entrée une coordonnée du maillage initial (ainsi qu'une normale, couleur et coordonnée de texture) exprimée dans le repère local de l'objet (pas nécessairement le repère du monde).
- - Et il renvoie en sortie (dans la variable prévue gl_Position) la coordonnée après placement dans le repère du monde ainsi que sa projection en perspective.
\(p_{out} = \) projection \(\times\) view \(\times\) model \(\times p\), avec
- - \(p\): la coordonnée du maillage dans son repère local (vecteur 4D).
- - model: la matrice (\(4\times 4\)) de transformation de l'espace local de l'objet vers le repère globale du monde.
- - view: la matrice (\(4\times 4\)) correspondant à l'orientation et la position de la caméra. Permet de passer du repère du monde vers l'espace "camera" ("view space").
- - projection: la matrice (\(4\times 4\)) de projection qui permet de passer de l'espace centré caméra vers le cube normalisé (Normalized Device Coordinates).
- - \(p_{out}\): la coordonnée (4D) de sortie exprimée dans le repère "Normalized Device Coordinates" (coordonnée uniquement visible entre [-1,1]).
Préparation des shaders à modifier
Pour éviter de modifier le shader appliqué à l'ensemble des objets, nous allons affecter un shader spécifique à certains objets uniquement. Le répertoire shaders/mesh_custom/ contient une copie des fichiers qui sont dans shaders/mesh/. Dans la suite, vous allez modifier le code des shaders décrits dans mesh_custom. Associons certains objets tels que le modèle de chameau, sphere et cube aux shaders modifiables. Pour cela, ajoutez les lignes suivantes à la fin de la fonction initialize().// Load a shader from a file opengl_shader_structure shader_custom; shader_custom.load( project::path + "shaders/mesh_custom/mesh_custom.vert.glsl", project::path + "shaders/mesh_custom/mesh_custom.frag.glsl"); // Affect the loaded shader to the mesh_drawable camel.shader = shader_custom; cube.shader = shader_custom; sphere.shader = shader_custom;
Transformation affines dans le shader
Considérons le cas où l'on souhaite appliquer une transformation affine supplémentaire sur les formes. > Modifiez dans le vertex shader (shaders/mesh_custom/mesh_custom.vert.glsl) la ligne suivante du shader "vec4 position = model * vec4(vertex_position, 1.0);" parmat4 M = transpose( mat4(2.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)); vec4 position = M * model * vec4(vertex_position, 1.0);

- - Bien que cela ne soit pas utile dans cet exemple, nous définissons la "transposée" de la matrice décrite ici. GLSL construit en fait la matrice par colonnes (et non par lignes). Lorsque l'on définit une matrice suivant une écriture "manuscrite" classique, il faut donc considérer sa transposée.
- - Le code que vous écrivez dans les fichiers .glsl n'est pas du C++, c'est du GLSL. Les fichiers de shaders sont re-compilés à chaque lancement du programme executable. Cela signifie que vous n'avez pas besoin de compiler le code C++ lorsque vous modifiez un shader, il suffit de relancer l'executable pour que la modification soit prise en compte.
> Retrouvez le principe des transformations affines vues en cours pour appliquer des translations.
> Que se passe-t-il si vous modifiez la toute dernière composante de la matrice en bas à droite de 1.0 à 2.0?. Expliquez pourquoi. Remarques:
-
- La transformation supplémentaire \(M\) est appliquée au sommet avant la multiplication par la matrice "model" qui applique elle-même des translations différentes sur les objets.
Si vous écrivez "model * M * vec4(vertex_position, 1.0)" (à la place de "M * model * vec4(vertex_position, 1.0)"), vous obtiendrez le même scaling, mais les objets seront à des positions différentes. -
- Rem. Utiliser la relation "model * M * vec4(vertex_position, 1.0)" revient à appliquer d'abord "M" sur les coordonnées locales de l'objet. Puis appliquer "model" par la suite.
- - Si vous supprimez la matrice "model" du calcul, alors les transformations définies dans le code C++ (ex. camel.model.translation = ...) ne sont plus appliquées. Dans le cas présent, les objets seront tous centrés sur l'origine et à leurs dimensions locales définies initialement lors de la génération du maillage.
Fragment shader
Le fragment shader utilisé dans le cas présent correspond au fichier shaders/mesh_custom/mesh_custom.frag.glsl. Ce code est exécuté après projection des sommets (et après "rasterization" des triangles) sur chaque pixel/fragment visible d'un triangle.L'objectif général d'un fragment shader est de considérer en entrée les valeurs interpolées sur les triangles à l'endroit du fragment courant (coordonnées, normale, couleur, textures) et de définir en sortie (variable FragColor) la couleur résultante à afficher.
Dans le cas présent, ce code implémente une illumination de Phong avec trois composantes: ambiante, diffuse, et spéculaire qui vous sera détaillée plus tard. > Modifiez la couleur de sortie des fragments en testant l'effet des lignes suivantes:
- - Le code est à écrire à la fin du fragment du fragment shader (juste avant l'accolade fermante).
- - Les lignes sont à tester séparément les unes après les autres.
- - Essayez de comprendre le résultat que vous obtenez (appelez si vous n'êtes pas sûr).
- - Notez que la couleur de sortie correspond à un vecteur dont les composantes (x,y,z,w) sont (rouge, vert, bleu, alpha). Chaque composante est censée être entre 0 (intensité minimale) et 1 (intensité maximale). La composante alpha n'est pas utilisée dans ce programme et peut être placée à une valeur arbitraire (elle sera utilisée plus tard pour des effets de transparence).
FragColor = vec4(1.0, 0.0, 0.0, 0.0);
FragColor = abs(vec4(cos(fragment.position.x), 0.0, 0.0, 0.0));
FragColor = abs(vec4(N.x, N.y, N.z, 0.0)); // rem. N represente la normale de la surface (dont la norme est 1).
FragColor = 0.8*vec4(color_shading, material.alpha * color_image_texture.a) + 0.2*abs(vec4(cos(10.0*fragment.position.z), 0.0, 0.0, 0.0));
if(cos(25.0*fragment.position.z)<-0.5f) { discard; // discard signifie l'arrêt du fragment shader // aucune couleur n'est affichée après discard // le pixel correspondant sera donc transparent. }

Paramètres uniformes et animation
Explication générale
Pour permettre de controler les shaders, des paramètres peuvent être passés depuis le programme C++. En particulier il est possible de passer des valeurs dites "uniform" aux shader. Un paramètre uniform correspond à une valeur qui est globale et commune à tout les sommets et fragments pendant l'execution du shader. Le shader actuel utilise déjà différent paramètres uniform pour envoyer les matrices de model et de projection, de la caméra, ou encore les paramètres de matériaux des objets. L'ajout d'un paramètre uniform se réalise en deux étapes:- 1) Envoyez ce paramètre depuis le programme C++
- 2) Définir et utiliser ce paramètre dans le shader
- Une variable uniforme est fixe pendant l'affichage d'un objet donné. Mais elle peut être modifiée d'un appel draw à l'autre - c'est donc différent d'un paramètre qui serait uniquement constant. Cela permet par exemple d'afficher deux fois le même objet, mais à une position et couleur différente. Ou encore avoir un paramètre temporel qui est modifié à chaque nouvelle frame.
- Il est possible d'envoyer d'autres types de paramètres plus complexes aux shaders
-
- - Soit un paramètre par sommet - on parle alors de VBO (Vertex Buffer Object). Il y a alors une valeur par sommet, et c'est sous cette forme que l'on envoie la position (coordonnées xyz) des sommets, leur normale, etc.
- - Soit un tableau de donnée générique - ce sont alors des buffers de texture. Il est alors nécessaire de réaliser soit même la correspondance entre un sommet/fragment et une position dans la texture.
Ajout d'un paramètre temporel
La classe scene_structure contient un attribut "timer" qui permet d'accéder au temps écoulé (exprimé en secondes) depuis le début du lacement de la boucle d'affichage. Affichez/vérifiez le contenu de cette variable sur la ligne de commande en appelant dans la fonction display_frame():std::cout << timer.t<< std::endl;
environment.uniform_generic.uniform_float["time"] = timer.t;
- - On va envoyer au shader un paramètre uniform de type "float".
- - Ce paramètre a pour valeur timer.t (cette valeur est différente à chaque nouvel affichage).
- - Le paramètre sera accessible dans le shader sous le nom de variable "time".
Remarques:
Dans le fichier de fragment shader(shaders/mesh_custom/mesh_custom.frag.glsl), ajoutez les éléments suivants:
La déclaration du paramètre "time" en tant que uniform (en début de fichier, à coté des autres paramètres uniform).
- - Le paramètre uniform n'est pas envoyé au shader par cette ligne, il est simplement stocké dans une variable. L'envoi effectif au shader est réalisé lors de l'appel à draw utilisant cette variable environment.
- - La variable "environment" est utilisée pour stocker des informations qui sont communes à tous les objets affichés. Elle contient notament la matrice (position/orientation) de la caméra ainsi que la projection en perspective.
uniform float time;
FragColor = abs(cos(time)) * vec4(color_shading, material.alpha * color_image_texture.a);
if (cos(25.0 * fragment.position.z+3.0*time) < -0.5f) { discard; }
Exercices
- > Utilisez ce paramètre temporel pour déformer le maillage dans le vertex shader de la manière suivante.
-
- (rem. les paramètres uniform sont accessibles dans le vertex et fragment shader de manière similaire.)
- > Ajoutez la possibilité de modifier la fréquence de l'oscillation de la déformation à partir de l'interface.
[Supplément] Conversion de votre scène en page web
Cette partie concerne uniquement les étudiants à l'aise avec Linux/Mac Les codes, et la librairie CGP sont fournies de manière à pouvoir être compilés sous la forme de page web qu'il est possible d'uploader sur un serveur.- Exemple du code de ce TP: Webpage
- 1) Installez emscripten en suivant la procédure expliquée ici.
- 2) Placez-vous dans le répertoire racine de l'exercice (là où ce situe le fichier CMakeLists.txt), et lancez le script
python scripts/linux_compile_emscripten.py
- 3) Si la compilation s'est bien déroulée, le site web peut être testé en lancant
emrun index.html
- - Ces commandes fonctionneront si vous disposez d'un Linux (ou sous Windows en utilisant WSL2 avec Windows11).
- - Exporter votre résultat en page web peut être intéressant pour valoriser et diffuser votre projet.
- - Lors de l'execution sur navigateur, les shaders sont executés suivant la norme WebGL, qui est proche mais légèrement différente d'OpenGL 3.3. Certaines fonctionalités peuvent être manquantes (ex. pas d'affichage de segments, de mode wireframe, etc.).
- - En fin d'année, si votre PC n'est pas en mesure de compiler avec emscripten mais que vous souhaitez exporter votre projet, demandez l'aide des enseignants qui pourront vous aider en réalisant la démarche sur leur machine.