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.
Lors de l'exécution du code, vous devez visualiser une scène 3D contenant
Une caméra contrôlable à la souris est également fournie. Les déplacements suivants sont possibles:

Code

Le code correspondant à cette scène se décompose en deux parties:

Remarques: Dans un premier temps, le code C++ situé dans scene.cpp (ainsi que de la bibliothèque) est déjà long et va vous sembler complexe, mais cela permet l'affichage d'une scène 3D complète dès à présent. Vous n'avez pas à comprendre l'intégralité en débutant le C++, mais petit à petit au cours des séances, vous serez en mesure de vous y habituer et de le comprendre.
Il est normal que des difficultés puissent provenir de différents points:
Dans l'ensemble des cas, n'hésitez pas à nous poser des questions lorsque vous ne comprenez pas certains points où que vous souhaitiez en savoir plus.

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)
Vous pouvez voir l'utilisation de "std::cout" au début de la fonction scene_structure::initialize() du fichier scene.cpp. Les autres affichages que vous pouvez voir sur la ligne de commande sont principalement réalisés dans le fichier main.cpp.

Commentaires

// 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:
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);
Remarques

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:
// dans scene.hpp
struct scene_structure {
    ...

    // Ajoutez votre variable de classe 
    // (par exemple après la ligne "mesh_drawable cube;")
    mesh_drawable sphere;

    ...
};
// Ecrire à la suite de "mesh sphere_mesh = mesh_primitive_sphere();"
sphere.initialize_data_on_gpu(sphere_mesh);
draw(sphere, environment);
> Recompilez et relancez le code. Une sphère blanche devrait désormais être affichée à l'origine.

Explication

  • - 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.

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])
> Relancez le code pour observer le résultat.
assets/sphere.jpg
  • 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.
assets/completion.jpg
Un IDE (ici Visual Studio) auto-complète et fourni automatiquement la liste des fonctions disponibles et paramètres attendus.
Pour des objets plus complexes que des primitives basiques, il peut être avantageux de charger des maillages depuis un fichier que l'on peut télécharger ou éditer à l'aide de modeleur 3D.
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:
mesh_drawable camel;
// 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 };
draw(camel, environment);
assets/camel.jpg
Apparté. "project::path" est une variable proposée par la bibliothèque CGP permettant d'adapter le chemin d'accès aux données. En effet, en fonction de votre système et approche de compilation, votre programme est potentiellement executé depuis différent répertoires, ce qui pourrait nécessiter d'adapter le chemin d'accès au répertoire shaders/. La variable project::path est automatiquement complétée au lancement du programme et tente de trouver les chemins relatifs possible entre le repertoire d'où vous lancez l'executable jusqu'à répertoire assets/.

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);
assets/camel_wireframe.jpg
Notez que par défaut, les arêtes sont affichées en bleu. Il est possible d'expliciter une couleur (r,g,b) quelconque en argument supplémentaire.
// 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);
Observez que les objets (ainsi que leur triangles) ne sont plus affichés dans un ordre correct vis à vis de leur position spatiale (désactivez l'affichage du mode wireframe et tournez la caméra pour mieux observer le phénomène).
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:
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.
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.
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) {
    ...
}
sans avoir à expliciter "== true". En C++ une condition est considérée comme valide tant que sa valeur est différente de 0 (ou false).
> Relancez votre programme et testez le fonctionnement du nouveau bouton de votre interface.

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);
> Relancez votre programme et testez le fonctionnement du slider.
Remarques: Cette fois la procédure était encore plus simple que pour le bouton. Nous n'avons pas à créer de variable intermédiaire. En effet, la translation suivant x du dromadaire est déjà stockée et accessible dans la variable "camel.model.translation.x" qui était utilisée à chaque affichage (lors de l'appel à "draw(camel, environment)"). Dans ce cas, il suffit de lier cette variable au slider pour permettre l'interaction de l'utilisateur.

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]\).
assets/perspective.png
Ce modèle, et donc l'espace visible du cône tronqué, est paramétré par:
La matrice \(4\times 4\) correspondante à ce modèle est la suivante:
\(\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:

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;
Remarques:
> Créez désormais un slider qui permet de modifier dynamiquement ce paramètre d'angle d'ouverture de caméra entre \([10^{\circ}, 150^{\circ}]\).

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:
Les shaders sont à écrire par le développeur et vont varier en fonction des effets/paramètres souhaités. Cependant les exemples de programmes de ces sessions pratiques sont fournis avec des shaders basiques, qui permettent notamment l'affichage de maillages dans la plupart des cas.
Dans la suite de cette partie, nous allons manipuler un exemple de shader pré-écrit, et nous verrons plus en détail le principe et l'échange de données dans la prochaine séance.

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;

...
Sans rentrer dans les détails de ce code, l'objectif de ce vertex shader est de réaliser l'étape de projection des sommets du maillage.
Formellement, la relation implémentée est la suivante:
\(p_{out} = \) projection \(\times\) view \(\times\) model \(\times p\), avec
Le reste des commandes permettent de gérer les dimensions entre vecteurs 3D et espace projectif 4D, ainsi que de gérer les normales, couleurs, et uv qui seront utilisées pour le calcul d'illumination dans le fragment shader. Les variables qualifiées de "uniform" sont des paramètres du shaders qui sont transmis depuis le code C++ juste avant l'affichage d'un objet.
L'un des points important (et potentiellement complexe à appréhender) est que ce programme est exécuté par la carte graphique à chaque nouvelle frame, et séparément pour chaque sommet. Notez par exemple qu'il n'y a pas de boucle sur les sommets dans ce code. Ainsi la variable d'entrée "vec3 position;" est différente pour chaque sommet, mais le code qui est exécuté est le même.
Plus précisément, ce code est exécuté en parallèle sur les sommets et cette parallélisation est gérée automatiquement par OpenGL. Notez que les cartes graphiques récentes possèdent plusieurs milliers de coeurs, et peuvent donc traiter des milliers de sommets en parallèle.
Grâce à cette exécution en parallèle sur la carte graphique, les calculs réalisés dans les shaders sont extrêmement efficaces. D'une manière générale toutes les opérations "couteuses" qui doivent être appliquées individuellement sur chaque sommet et à chaque frame aura avantage à être programmées dans le vertex shader plutôt que dans le code C++.

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);" par
mat4 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);
assets/shader_modif.jpg
Nous venons d'appliquer un scaling suivant l'axe x qui a allongé toutes les formes utilisant ce shader suivant cet axe.
Remarques:
> Appliquez des scaling suivant d'autres axes (ou des "shearing"/cisaillements). Il est également possible de définir des rotations.
> 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:

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:
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.
}
Remarque: Il s'agit d'exemples "de démonstrations" pour l'instant, mais notez d'une manière générale que la modification du fragment shader permet d'obtenir un contrôle très précis (calcul réalisé sur chaque pixel) qui serait complexe (ou très couteux) de réaliser hors d'un shader.
assets/shader_discard.jpg

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:
Remarques:

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;
Une fois l'affichage vérifié (des valeurs s'incrémentant continuellement), enlevez par la suite cette affichage pour éviter de surcharger votre ligne de commande et de ralentir votre programme.
La variable "environnement" (de type environment_structure définie dans le fichier environment.hpp) possède l'attribut "uniform_generic" (type uniform_generic_structure) qui permet d'ajouter aisément des valeurs envoyés au shader comme paramètre uniform.
Ajoutez la ligne suivante dans la fonction display_frame()
environment.uniform_generic.uniform_float["time"] = timer.t;
Cette ligne indique que
Remarques:
  • - 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.
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).
uniform float time;
Utilisez ce paramètre pour modifier la couleur de la forme
FragColor = abs(cos(time)) * vec4(color_shading, material.alpha * color_image_texture.a);
Ou encore, testez par exemple à la place
if (cos(25.0 * fragment.position.z+3.0*time) < -0.5f) {
    discard;
}

Exercices

[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.
Le principe est de compiler le code C++ vers du WebAssembly (code de bas niveau équivalent d'un assembleur pour un navigateur) par le biais de l'outil emscripten.
La démarche est la suivante:
python scripts/linux_compile_emscripten.py
emrun index.html
Remarques: