INF443 Introduction maillages dans une scène 3D
Scène basique et controle
Compilez et exécutez le code situé dans le répertoire scenes/inf443/01_introduction- Vous devez suivre la même démarche expliquée dans le tutoriel de compilation (CMake -> 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.
- - Zoom: clic droit suivi d'un déplacement de la souris haut/bas.
- - Panning (= translation dans le plan de la caméra): CTRL + clic gauche + déplacement de la souris
- - Avance/Recule (= translation dans le plan orthogonal à la caméra): CTRL + clic droit + déplacement haut/bas de la souris.
Code
Le code correspondant à cette scène se décompose en deux parties:- 1- Le fichier main.cpp situé dans le répertoire scenes/inf443/01_introduction/src/
-
- Ce fichier contient l'intégralité de la mise en place spécifique de la scène. Vous allez principalement éditer le code C++ dans ce fichier.
- 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 library/cgp/. Vous n'aurez généralement pas besoin de modifier ces fichiers.
-
- Une documentation basique de la bibliothèque est disponible ici: CGP Quick Tour.
- - 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.Fonction main et organisation générale
Observez rapidement la fonction "int main()" du fichier main.cpp.- - Tout programme C++ doit nécessairement avoir une fonction dénommée "main", qui correspond au point de départ du programme.
- - A plusieurs reprises, vous croiserez la commande "std::cout<<" ... "<<std::endl;". Il s'agit de la commande d'écriture standard 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.
- - 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 */
- 1. Une étape d'initialisation des données par l'appel à la fonction 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 display_scene() (et display_gui) à chaque frame.
-
- L'objectif de la fonction display_scene() 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.
mesh sphere_mesh = mesh_primitive_sphere();
- - 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 unitaire. 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 globale de type mesh_drawable (la variable globale est écrite en haut du fichier main.cpp, après "mesh_drawable ground;" par exemple).
mesh_drawable sphere;
- b. Initialisez le contenu de sphere à partir de sphere_mesh dans la fonction initialize()
// Ecrire à la suite de "mesh sphere_mesh = mesh_primitive_sphere();" sphere.initialize(sphere_mesh, "Sphere");
- c. Demandez l'affichage de la sphere dans la fonction display_scene() (par exemple après "draw(ground, environment);")
draw(sphere, environment);
Explication
- - L'étape a. consiste à déclarer une variable avec son type. La variable n'est pas initialisée à ce stade. En C++, les variables déclarées en haut des fichiers sources (en dehors des fonctions) sont appelées "variables globales". Elles sont accessibles dans toutes les fonctions de ce fichier, ce qui permet de partager des valeurs entre plusieurs fonctions.
- - Il est généralement préférable d'éviter de déclarer trop de variables globales dans un programme complexe (en C++ comme dans tout autre langage). En effet, du fait de leur portée globale, il peut être difficile de suivre quelle variable est modifiée dans quelle fonction. Il est plus simple de suivre une logique de "variables d'entrée" en tant que paramètre, et de "variable de retour" pour la sortie. En utilisant des variables de portée globale, il y a également un risque d'entrer en conflit avec d'autres variables locales qui seraient déclarées avec le même nom.
- - Dans les codes des prochaines séances, nous utiliserons un objet appelé scene dans laquelle les variables devant être partagées entre la fonction d'initialisation et d'affichage seront déclarées comme des variables de la classe (en suivant une programmation "orientée objet").
- - L'étape b. permet d'envoyer les données contenues dans la classe mesh sur la carte graphique. 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 la séance prochaine.
- - L'étape c. est 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\) à chaque instant tous les objets de la scène doivent être ré-affichés.
Modification de la sphère
Il est possible d'adapter des paramètres globaux de l'objet tels que sa couleur, position, dimension en modifiant certains paramètres de la classe mesh_drawable.Par exemple écrivez dans la fonction initialize() après sphere.initialize(...):
// to add after "sphere.initialize(sphere_mesh, "Sphere");" sphere.transform.scaling = 0.2f; // coordinates are multiplied by 0.2 in the shader sphere.transform.translation = {1,2,0}; // coordinates are offseted by {1,2,0} in the shader sphere.shading.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 fournie la création pré-codées de primitives basiques (sphères, cube, cylinder, cone, etc) par le biais de l'appel "mesh_primitive_xxx". Mais pour des objets plus complexes, 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:
- - Créez la variable globale suivante:
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("assets/camel.obj"); // Initialisation classique de la structure mesh_drawable camel.initialize(camel_mesh, "obj mesh"); // Ajustement de la taille et position de la forme camel.transform.scaling = 0.5f; camel.transform.translation = { -1,1,0.5f };
- - Ajouter l'appel à l'affichage dans la fonction display_scene()
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_scene() 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_scene()
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 globale booléenne qui va permettre de stocker s'il faut afficher ou non le maillage en wireframe.
bool gui_display_wireframe = false;
- 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_scene() 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); 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.transform.translation.x, -2.0f, 2.0f);
Projection
La bibliothèque CGP propose des modèles de projections de caméra pré-programmés dans le cas de représentation standard. Un modèle de projection en perspective, et un modèle de projection orthogonale.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: "environment.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(environment.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 "environment.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():
environment.projection.perspective_data.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.
Projection orthogonale
La bibliothèque propose également un modèle pré-codé de projection orthogonale. Dans ce cas, le modèle est paramétré par les dimensions dans les 3 directions (left, right, bottom, up, front, back) par rapport au point central. Ajoutez ces lignes à la fin de la fonction initialize() pour obtenir une projection orthogonale.// Change le type de projection à orthogonale environment.projection.type = camera_perspective_type::orthographic; // Autorise une vue en profondeur pour des objets // situés à une distance comprise entre -10 et +10 environment.projection.orthographic_data.back = -10; environment.projection.orthographic_data.front = 10;
Remarque: La projection orthogonale représente un modèle indépendamment du point de vue - ce qui explique son utilisation pour des représentations précises de longueurs, utile typiquement lors de la modélisation d'un objet suivant les directions x/y/z sans déformation.
Par contre, il n'y a pas d'effet de perspective (/éloignement) - les objets éloignés sont aussi grands que ceux proches de la caméra, et la caméra ne semble pas se déplacer vers l'avant/arrière: les objets apparaissent/disparaissent directement dans le champ de vision à leur dimension fixe. Cette représentation est généralement non adaptée pour naviguer dans une scène 3D, car non naturelle.
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 individuel 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/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 // OpenGL 3.3 shader // Vertex shader - this code is executed for every vertex of the shape // Inputs coming from VBOs layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; layout (location = 2) in vec3 color; layout (location = 3) in vec2 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 (perspective ou orthogonale).
\(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 (perspective ou orthogonal) 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]).
Transformation affines dans le shader
Il est possible de modifier directement le code des shaders, et ainsi de modifier l'apparence des objets.Considérons le cas où l'on souhaite appliquer une transformation affine supplémentaire sur les formes. > Modifiez la ligne suivante du shader "vec4 p = model * vec4(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 p = M * model * vec4(position, 1.0);
- - Par défaut l'ensemble des formes utilisent ce shader (donc toutes les formes sont modifiées), sauf le repère d'origine qui utilise sont propre shader (mesh_reference) afin de garder une référence visuelle.
- - 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.
> 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(position, 1.0)" (à la place de "M * model * vec4(position, 1.0)"), vous obtiendrez le même scaling, mais les objets seront à des positions différentes. -
- Rem. Utiliser la relation "model * M * vec4(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.transform.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/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);
FragColor = abs(vec4(cos(fragment.position.x),0,0,0));
FragColor = abs(vec4(N.x,N.y,N.z,0)); // rem. N represente la normale de la surface (dont la norme est 1).
FragColor = 0.8*vec4(color_shading, alpha * color_image_texture.a) + 0.2*abs(vec4(cos(10*fragment.position.z),0,0,0));
if(cos(25*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. }