OpenGL

Dessiner des polygones

Alexander Overvoorde

Traducteur

Alexandre Laurent

- Titre : OpenGL

- Auteur : Alexander Overvoorde

- Traducteur : Alexandre Laurent

- Parution : 7 février 2017

- Licence : Le contenu de cet article est rédigé par Alexander Overvoorde et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Navigation

- I. Le pipeline graphique

- II. Sommets en entrée

- III. Shaders

- III-A. Vertex shader

- III-B. Fragment shader

- III-C. Compiler les shaders

- III-D. Combiner les shaders en un programme

- III-E. Faire le lien entre les données de sommets et les attributs

- III-F. Vertex Array Objects

- IV. Dessin

- V. Variables uniformes

- VI. Ajoutez plus de couleurs

- VII. Tampons d'éléments

- VIII. Exercices

- IX. Remerciements

- Navigation

Navigation

Le contenu dynamique ne peut pas être affiché dans ce support, veuillez consulter la page en ligne.

Tutoriel précédent : création du contexte

Sommaire

Tutoriel suivant : textures

I - Le pipeline graphique

En apprenant OpenGL, vous avez choisi de faire tout le dur travail vous-même. Inévitablement, cela signifie que vous allez être jeté dans les profondeurs, mais, une fois que vous avez compris les bases, vous allez voir que faire les choses à la dure n'est pas si difficile après tout. De plus, les exercices à la fin de ce chapitre vous montreront que vous avez le contrôle absolu sur le processus de rendu grâce à la méthode moderne.

Le pipeline graphique couvre toutes les étapes à suivre afin de produire l'image finale grâce aux données en entrée. Je vais expliquer ces étapes en m'aidant de la figure suivante.

Tout commence par les sommets (« vertices »). Ce sont des points à partir desquels les formes telles que le triangle seront construites. Chacun de ces points est stocké avec des attributs et c'est à vous de décider de ce que constituent ces attributs. Les attributs habituels sont la position 3D dans le monde et les coordonnées de texture.

Le vertex shader est un petit programme s'exécutant sur votre carte graphique et qui traite chacun de ces sommets individuellement. C'est l'étape où la transformation de la perspective prend place, projetant les sommets du monde 3D sur l'écran 2D ! Le programme transfère les attributs importants comme la couleur et les coordonnées de texture à la suite du pipeline.

Après la transformation des sommets, la carte graphique va construire les triangles, lignes et points à partir de ceux-ci. Ces formes sont appelées des primitives, car elles constituent la base de formes plus complexes. Il y a d'autres modes de dessin que l'on peut choisir, comme les séries de triangles et les séries de lignes. Ils permettent de réduire le nombre de sommets nécessaire à transférer si vous souhaitez créer des objets où l'ensemble des primitives se suit, comme avec une ligne constituée de plusieurs segments.

L'étape suivante, le geometry shader, est totalement optionnelle et n'a été introduite que récemment. Contrairement au vertex shader, le geometry shader peut produire plus de données qu'il n'en reçoit. Il prend, comme entrée, les primitives à partir de l'étape d'assemblage de forme et peut soit passer une primitive à la suite du pipeline, en la modifiant, ou complètement la supprimer, ou encore la remplacer par d'autre(s) primitive(s). Sachant que la communication entre le GPU et le reste du PC est relativement lente, cette étape peut aider à réduire le nombre de données devant être transférées. Par exemple, pour un jeu en voxel, vous pouvez passer des sommets en tant que points, avec des attributs représentant leur position dans le monde, la couleur et le matériel, et le cube pourra être produit dans le geometry shader à partir du point.

Après avoir composé la liste finale des formes et après l'avoir convertie en coordonnées écran, le rasteriseur transforme les parties visibles des formes en fragments de la taille du pixel. Les attributs provenant du vertex shader ou du geometry shader sont interpolés et passés en entrée au fragment shader pour chaque fragment. Comme vous pouvez le voir sur l'image, les couleurs composent un doux dégradé sur les fragments constituant le triangle, même si nous n'avons spécifié que trois points.

Le fragment shader traite chaque fragment avec ses attributs interpolés et doit fournir la couleur finale. Cela se fait généralement en échantillonnant une texture grâce aux coordonnées de texture interpolées provenant du vertex shader ou simplement en définissant une couleur. Dans des scénarios plus aboutis, il peut aussi y avoir des calculs pour la lumière et l'ombre et des effets spéciaux dans ce programme. Le shader possède aussi la possibilité de retirer certains fragments, de telle sorte qu'il est possible de voir à travers la forme.

Finalement, le résultat est une composition de la fusion de tous les fragments de forme, et sur lesquels on applique un test de profondeur (« depth test ») et de pochoir (« stencil test »). Pour le moment, tout ce que vous devez savoir à propos de ces deux derniers est qu'ils vous permettent d'utiliser des règles supplémentaires pour éliminer certains fragments et laisser les autres. Par exemple, si un triangle est caché par un second, le fragment du triangle le plus proche doit être affiché à l'écran.

Maintenant que vous savez comment votre carte graphique transforme un tableau de sommets en une image à l'écran, allons-y !

II - Sommets en entrée

La première chose est de déterminer les données que la carte graphique nécessite pour dessiner votre scène correctement. Comme dit précédemment, les données viennent sous la forme d'attributs de sommets. Vous êtes libre de choisir n'importe quel type d'attributs que vous souhaitez, mais cela démarre inévitablement avec la position dans le monde. Que ce soit pour des graphismes 2D ou 3D, c'est cet attribut qui déterminera où et de quelle façon les objets vont être sur votre écran.

Coordonnées du périphérique

Lorsque vos sommets ont été traités par le pipeline décrit ci-dessus, leurs coordonnées ont été transformées en coordonnées du périphérique. Les coordonnées X et Y du périphérique sont comprises entre -1 et 1.

Tout comme dans un graphe, le centre est aux coordonnées (0,0) et l'axe des y est positif au-dessus du centre. Cela semble illogique, car les applications graphiques situent le point (0,0) dans le coin supérieur gauche et (largeur, hauteur) dans le coin inférieur droit, mais c'est une méthode excellente pour simplifier les calculs 3D et être indépendant de la résolution.

Le triangle ci-dessus est constitué de trois sommets positionnés en (0, 0.5), (0.5, -0.5) et (-0.5, -0.5) en suivant le sens des aiguilles d'une montre. Il est évident que la seule variation entre les sommets est la position, donc c'est l'unique attribut dont nous avons besoin. Comme nous utilisons directement des coordonnées de périphérique, seuls X et Y suffisent pour déterminer la position.

OpenGL s'attend à recevoir tous les sommets dans un seul tableau, ce qui peut être source de confusion au début. Pour comprendre le format de ce tableau, voyons à quoi il ressemblerait pour notre triangle.

float vertices[] = {
0.0f,  0.5f, // Sommet 1 (X, Y)
0.5f, -0.5f, // Sommet 2 (X, Y)
-0.5f, -0.5f  // Sommet 3 (X, Y)
};

Comme vous pouvez le voir, le tableau doit simplement être une liste de tous les sommets avec leurs attributs mis bout à bout. L'ordre des attributs n'importe pas, tant que vous gardez le même ordre pour chaque sommet. L'ordre des sommets n'a pas besoin d'être séquentiel (c'est-à-dire l'ordre nécessaire pour dessiner la forme), mais cela nous demanderait de fournir des données additionnelles sous la forme d'un tampon d'éléments. Cela sera détaillé à la fin du chapitre afin de ne pas compliquer les choses.

La prochaine étape est d'envoyer les données des sommets à la carte graphique. C'est important, car la mémoire de votre carte graphique est beaucoup plus rapide et que vous ne voulez pas envoyer les données à chaque fois que la scène doit être affichée (environ 60 fois par seconde).

Cela se fait en créant un Vertex Buffer Object (VBO) :

GLuint vbo;
glGenBuffers(1, &vbo); // Génère 1 tampon

La mémoire est gérée par OpenGL, donc au lieu de recevoir un pointeur, vous avez un nombre positif. GLuint est simplement un substitut multiplateforme pour les unsigned int, tout comme GLint l'est pour les int. Vous allez avoir besoin de ce numéro pour activer le VBO et le détruire lorsque vous aurez fini de l'utiliser.

Pour envoyer les données, vous devez d'abord rendre l'objet actif en appelant glBindBuffer :

glBindBuffer(GL_ARRAY_BUFFER, vbo);

Comme laisse penser le GL_ARRAY_BUFFER, il y a d'autres types de tampons, mais ils ne sont pas importants pour le moment. Cette ligne permet d'assigner le VBO que nous venons de créer comme le array buffer actif. Maintenant qu'il est actif, nous pouvons y copier des données.

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

Remarquez que cette fonction n'utilise pas l'identifiant de notre VBO, mais se sert du tampon actif. Le second paramètre indique la taille en octets. Le dernier paramètre est très important et sa valeur dépend de l'utilisation des données. Je vais décrire ceux en rapport avec le rendu :

- GL_STATIC_DRAW : les données seront transférées une seule fois et dessinées plusieurs fois (par exemple, le monde) ;

- GL_DYNAMIC_DRAW : les données seront modifiées quelques fois, mais dessinées plus de fois ;

- GL_STREAM_DRAW : les données seront modifiées presque toutes les fois qu'on les dessine (par exemple : l'interface utilisateur).

Le nombre d'utilisations va déterminer dans quel type de mémoire les données seront stockées sur la carte graphique pour une meilleure efficacité. Par exemple, les données des VBO avec GL_STREAM_DRAW pourront être placées dans une mémoire permettant une écriture plus rapide en contrepartie d'un affichage un peu plus lent.

Les sommets et leurs attributs ont été copiés sur la carte graphique, mais nous ne sommes pas encore prêts à les utiliser. Rappelez-vous que nous pouvons déterminer n'importe quels attributs dans n'importe quel ordre, donc, maintenant nous devons expliquer à la carte graphique comment gérer ces attributs. C'est là que vous allez vous rendre compte de la réelle flexibilité d'OpenGL moderne.

III - Shaders

Comme nous l'avons vu précédemment, il y a trois types de shader qui traiteront vos données de sommets. Chaque étape a un but précis et, dans les vieilles versions d'OpenGL, vous ne pouviez paramétrer que très légèrement ce que la carte graphique allait faire avec vos données. Avec les versions modernes d'OpenGL, c'est à vous de dire ce que fait la carte graphique. C'est pourquoi il est possible de décider dans l'application quels sont les attributs de sommets que vous voulez. Vous devez implémenter le vertex shader et le fragment shader afin d'obtenir quelque chose à l'écran. Le geometry shader est optionnel et sera vu plus tard.

Les shaders sont écrits dans un langage proche du C appelé GLSL (OpenGL Shading Language). OpenGL compilera votre programme à partir des sources lors de l'exécution et le copiera sur la carte graphique. Chaque version d'OpenGL possède sa propre version du langage de shader ayant son ensemble de fonctionnalités. Ici nous allons utiliser GLSL 1.50. Ce numéro de version peut sembler sorti de nulle part lorsque l'on utilise OpenGL 3.2, mais il faut se rappeler que les shaders ont été introduits dans OpenGL 2.0 en version GLSL 1.10. À partir d'OpenGL 3.3, ce problème a été corrigé et la version du GLSL correspond à la version d'OpenGL.

III-A - Vertex shader

Le vertex shader est un programme de la carte graphique qui traite chaque sommet et ses attributs tels qu'ils proviennent du tableau de sommets. Son travail est de générer la position finale du sommet en coordonnées du périphérique et de produire les données dont a besoin le fragment shader. C'est pourquoi la transformation 3D doit être réalisée ici. Le fragment shader dépend des attributs comme la couleur et les coordonnées de textures, qui sont directement transférées, sans calcul supplémentaire, des entrées aux sorties du vertex shader.

Souvenez-vous que notre position du sommet est déjà en coordonnées de périphérique et qu'il n'y a pas d'autres attributs, donc le vertex shader sera très dépouillé.

#version 150

in vec2 position;

void main()
{
gl_Position = vec4(position, 0.0, 1.0);
}

La directive du préprocesseur #version est utilisée pour indiquer que le code qui va suivre est un code GLSL 1.50. Ensuite, nous spécifions qu'il n'y a qu'un seul attribut, la position. En plus des types de base du C, GLSL possède des types pour les vecteurs et la matrice : vec* et mat*. Le type pour les valeurs de ces ensembles est toujours float. Le nombre avec l'identifiant vec indique le nombre de composants (x, y, z, w) et le nombre après mat indique le nombre de lignes/colonnes. Comme l'attribut position n'est constitué que des coordonnées X et Y, le vec2 est le bon choix.

Vous pouvez être créatif en travaillant avec ces vecteurs. Dans l'exemple ci-dessus, nous utilisons un raccourci pour définir les deux premiers composants du vec4 à partir du vec2. Ces deux lignes sont identiques :

gl_Position = vec4(position, 0.0, 1.0);
gl_Position = vec4(position.x, position.y, 0.0, 1.0);

Lorsque vous travaillez avec les couleurs, vous pouvez aussi accéder aux composants individuellement avec r, g, b et a à la place de x, y, z et w. Cela ne change rien, mais aide à la clarté du code.

La position finale du sommet est assignée à la variable spéciale : gl_Position, car la position est nécessaire à l'assembleur de primitives et à plein d'autres traitements internes. Pour que ces traitements fonctionnent correctement, la dernière valeur, w, doit être à 1.0f. À part cela, vous être libre de faire tout ce que vous souhaitez avec les attributs et vous allez voir comment les copier en sortie lorsque nous allons ajouter de la couleur à notre triangle dans la suite du chapitre.

III-B - Fragment shader

La sortie du vertex shader est interpolée sur tous les pixels de l'écran couverts par une primitive. Ces pixels sont appelés des fragments et c'est sur ceux-ci que le fragment shader travaille. Tout comme pour le vertex shader, il y a une sortie obligatoire : la couleur finale du fragment. C'est à vous d'écrire le code pour calculer cette couleur à partir des couleurs des sommets, des coordonnées de texture, ou de toute autre donnée provenant du vertex shader.

Notre triangle n'est constitué que de pixels blancs, donc le fragment shader ne fait que générer cette couleur à chaque fois :

#version 150

out vec4 outColor;

void main()
{
outColor = vec4(1.0, 1.0, 1.0, 1.0);
}

Vous allez immédiatement remarquer que nous n'utilisons pas de variable interne pour fournir la couleur, telle que gl_FragColor. Cela est dû au fait qu'un fragment shader peut fournir plusieurs couleurs et nous allons voir comment gérer cela lorsque nous allons charger ces shaders. La variable outColor utilise le type vec4, car chaque couleur est constituée de rouge, vert, bleu et d'un composant pour l'alpha. Les couleurs en OpenGL sont généralement représentées avec des nombres à virgule flottante, entre 0.0 et 1.0 au lieu des classiques 0 et 255.

III-C - Compiler les shaders

La compilation des shaders est simple une fois que vous avez chargé le code source (soit à partir d'un fichier, soit à partir d'une chaîne de caractères en dur dans le code). Tout comme pour les tampons de sommets, cela débute par la création d'un objet de shader puis par le chargement des données dans l'objet.

GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexSource, NULL);

Contrairement aux VBO, vous pouvez simplement passer une référence en argument des fonctions des shaders au lieu de le rendre actif ou quelque chose de ce genre. La fonction glShaderSource (doc) peut prendre plusieurs chaînes de caractères du code source dans un tableau, mais vous allez généralement avoir votre code source dans un tableau de caractères. Le dernier paramètre peut contenir un tableau de longueur des chaînes de caractères du code source. En passant NULL, la fonction s'arrêtera au caractère '\0'.

Il ne reste que la compilation du shader en code pouvant être exécuté par la carte graphique :

glCompileShader(vertexShader);

Sachez que si le shader ne peut pas être compilé, par exemple, à cause d'une erreur de syntaxe, la fonction glGetError (doc) ne signalera pas d'erreur ! Lisez le bloc ci-dessous pour savoir comment déboguer les shaders.

Vérifier qu'un shader s'est compilé correctement

GLint status;
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &status);

Si la variable status est égale à GL_TRUE, alors votre shader a été compilé correctement.

Récupérer le journal de compilation

char buffer[512];
glGetShaderInfoLog(vertexShader, 512, NULL, buffer);

Cela va sauvegarder les premiers 511 octets + le caractère '\0' du journal de compilation dans le tampon spécifié. Le journal peut aussi contenir des avertissements utiles même lorsque la compilation a été réalisée. Il est donc toujours utile de le vérifier lorsque vous développez vos shaders.

Le fragment shader est compilé exactement de la même manière :

GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
glCompileShader(fragmentShader);

Encore une fois, assurez-vous de vérifier que votre shader a été compilé, car cela vous évitera des maux de tête plus tard.

III-D - Combiner les shaders en un programme

Jusqu'à présent, le vertex et le fragment shader ont été deux objets séparés, alors qu'ils ont été programmés pour travailler ensemble, ils ne sont toutefois pas encore connectés. Cette connexion est réalisée en créant un programme à partir de ces deux shaders.

GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);

Comme un fragment shader peut écrire dans plusieurs tampons, vous devez spécifier explicitement quelle sortie du shader correspond à quel tampon. Cela doit se faire avant l'édition de liens du programme. Toutefois, comme la valeur par défaut est zéro et qu'il n'y a qu'une sortie pour le moment, la ligne suivante n'est pas nécessaire :

glBindFragDataLocation(shaderProgram, 0, "outColor");

Utilisez la fonction glDrawBuffers lors d'un rendu dans plusieurs tampons, car seule la première sortie sera activée par défaut.

Après avoir attaché le fragment et le vertex shader ensemble, la connexion est réalisée par une liaison du programme. Il est possible de faire des changements sur les shaders après qu'ils ont été ajoutés à un programme (ou à plusieurs programmes !), mais le résultat ne changera pas tant que le programme n'a pas été relié. Il est aussi possible d'attacher plusieurs shaders pour la même étape (par exemple, le fragment shader) si ces morceaux forment ensemble un shader complet. Un objet shader peut être détruit avec la fonction glDeleteShader (doc), mais n'est pas retiré avant que le shader ait été détaché de tous les programmes avec glDetachShader (doc).

glLinkProgram(shaderProgram);

Pour commencer à utiliser les shaders dans le programme, vous devez appeler :

glUseProgram(shaderProgram);

Tout comme pour le tampon de sommets, seul un programme peut être actif à la fois.

III-E - Faire le lien entre les données de sommets et les attributs

Bien que nous ayons notre donnée de sommets et les shaders, OpenGL ne sait toujours pas comment les attributs sont formés et ordonnés. Votre premier besoin est d'obtenir une référence sur la variable d'entrée position du vertex shader.

GLint posAttrib = glGetAttribLocation(shaderProgram, "position");

L'emplacement est un nombre dépendant de l'ordre des définitions des entrées. La première et l'unique variable d'entrée position dans cet exemple sera toujours placée en 0.

Avec une référence sur l'entrée, vous pouvez spécifier comment les données de cette entrée sont récupérées du tableau :

glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);

Le premier paramètre référence l'entrée. Le second indique le nombre de valeurs de cette entrée, qui est le nombre de composants du vec. Le troisième paramètre indique le type de chaque composant et le quatrième indique si les valeurs d'entrées doivent être normalisées entre -1.0 et 1.0 (ou 0.0 et 1.0 selon le format) si ce ne sont pas des nombres à virgule flottante.

Les deux derniers paramètres sont sans doute les plus importants car ils indiquent comment l'attribut est disposé dans le tableau de sommets. Le premier nombre indique le pas (stride), ou combien d'octets il y a entre chaque attribut position du tableau. La valeur 0 signifie qu'il n'y a pas de données entre celles-ci. C'est notre cas si la position de chaque sommet est immédiatement suivie de la position du prochain sommet. Le dernier paramètre indique le décalage (offset), à combien d'octets le premier attribut se trouve. Comme il n'y a pas d'autres attributs, c'est aussi 0.

Il est important de savoir que la fonction ne sauvegardera pas seulement le pas et le décalage, mais aussi le VBO actuellement lié à GL_ARRAY_BUFFER. Cela signifie que vous n'avez pas à lier explicitement le VBO adéquat pour appliquer les fonctions de dessin sur celui-ci. Cela implique aussi que vous pouvez utiliser un VBO différent pour chaque attribut.

Ne vous inquiétez pas si vous ne comprenez pas cela parfaitement. Nous allons bientôt voir comment ajouter plus d'attributs.

glEnableVertexAttribArray(posAttrib);

Et enfin, mais ce n'est pas le moins important, le tableau d'attribut des sommets doit être activé.

III-F - Vertex Array Objects

Vous pouvez imaginer que les programmes graphiques réels utilisent de nombreux shaders et agencements de sommets pour gérer une grande variété d'effets spéciaux. Le changement du programme de shaders actifs est assez simple avec la fonction glUseProgram, (doc), mais il serait gênant si vous deviez reconfigurer tous les attributs une nouvelle fois.

Heureusement, OpenGL apporte une solution avec les Vertex Array Objects (VAO). Les VAO stockent tous les liens entre les attributs de vos VBO et les données brutes de sommets.

Un VAO est créé de la même manière qu'un VBO :

GLuint vao;
glGenVertexArrays(1, &vao);

Pour commencer à l'utiliser, liez-le :

glBindVertexArray(vao);

Dès que vous avez lié un VAO, chaque fois que vous appelez glVertexAttribPointer (doc), cette information sera stockée dans ce VAO. Cela rend le changement de données de sommets et de formats de sommets aussi simple que de lier différents VAO ! Rappelez-vous simplement qu'un VAO ne stocke aucune des données de sommets lui-même, il ne fait que référencer les VBO que vous avez créés et indiquer comment récupérer les valeurs des attributs à partir de ceux-ci.

Comme seuls les appels faits après la liaison du VAO vont s'enregistrer, assurez-vous de le créer et de le lier au début de votre programme. N'importe quel tampon de sommets et tampon d'éléments liés avant cela sera ignoré.

IV - Dessin

Maintenant que vous avez chargé vos données de sommets, créé les programmes de shaders et lié les données aux attributs, vous être prêt pour dessiner le triangle. Le VAO qui a été utilisé pour stocker les informations des attributs est déjà lié, donc vous n'avez pas à vous inquiéter de cela. Tout ce qu'il vous reste à faire est d'appeler la fonction glDrawArrays (doc) dans la boucle principale.

glDrawArrays(GL_TRIANGLES, 0, 3);

Le premier paramètre indique le type de primitives (généralement des points, des lignes ou des triangles), le second paramètre indique combien de sommets doivent être ignorés au début et le dernier paramètre indique le nombre de sommets (pas les primitives) à traiter.

Lorsque vous exécutez le programme, vous devriez voir ceci :

Si vous ne voyez rien, assurez-vous que les shaders ont été compilés correctement, que le programme a été lié correctement, que le tableau d'attribut a été activé, que le VAO a été lié avant la spécification des attributs, que les données de sommets sont correctes et que la fonction glGetError (doc) retourne 0. Si vous ne pouvez pas trouver le problème, essayez en comparant votre code à celui-ci.

V - Variables uniformes

Actuellement, la couleur blanche du triangle a été écrite en dur dans le code du shader, mais que se passe-t-il si vous souhaitez la changer après la compilation du shader ? En effet, il existe une autre méthode pour passer des données à vos programmes de shaders. Cette autre façon est appelée variables uniformes (« uniforms »). Ce sont principalement des variables globales, possédant la même valeur pour tous les sommets et/ou tous les fragments. Pour montrer comment les utiliser, faisons en sorte que l'on puisse changer la couleur du triangle à partir du programme.

En obtenant la couleur du fragment shader à partir d'une variable uniforme, celui-ci ressemblera à ceci :

#version 150

uniform vec3 triangleColor;

out vec4 outColor;

void main()
{
outColor = vec4(triangleColor, 1.0);
}

Le dernier composant de la couleur de sortie est la transparence, ce qui ne nous intéresse pas pour le moment. Si vous exécutez ce programme, vous allez voir le triangle noir, car la valeur de la variable triangleColor n'a pas été définie.

Changer la valeur d'une variable uniforme s'effectue de la même façon que définir un attribut de sommet, vous devez d'abord récupérer l'emplacement :

GLint uniColor = glGetUniformLocation(shaderProgram, "triangleColor");

Les valeurs des variables uniformes sont définies avec les fonctions glUniformXY, (doc), où X est le nombre de composants et Y le type. Les types classiques sont f (float), d (double) et i (entier).

glUniform3f(uniColor, 1.0f, 0.0f, 0.0f);

Si vous exécutez votre programme maintenant, vous allez voir le triangle en rouge. Pour rendre les choses un peu plus amusantes, essayez de varier la couleur du triangle selon le temps en faisant quelque chose comme cela dans votre boucle principale :

auto t_start = std::chrono::high_resolution_clock::now();

...

auto t_now = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration_cast<std::chrono::duration<float>>(t_now - t_start).count();

glUniform3f(uniColor, (sin(time * 4.0f) + 1.0f) / 2.0f, 0.0f, 0.0f);

Bien que cet exemple puisse ne pas être très amusant, il montre que les variables uniformes sont essentielles pour contrôler le comportement des shaders à l'exécution. Les attributs de sommets sont, quant à eux, idéaux pour la description d'un sommet.

Le contenu dynamique ne peut pas être affiché dans ce support, veuillez consulter la page en ligne.

Regardez ce code, si vous avez des soucis à faire fonctionne le programme.

VI - Ajoutez plus de couleurs

Bien que les variables uniformes aient leur utilité, la couleur est une chose que l'on préfère spécifier pour chaque coin d'un triangle ! Ajoutons un attribut de couleur aux sommets.

Nous allons commencer par ajouter les attributs supplémentaires aux données du sommet. La transparence n'est pas vraiment pertinente, donc nous n'ajoutons que les composantes rouge, vert et bleu.

float vertices[] = {
0.0f,  0.5f, 1.0f, 0.0f, 0.0f, // Vertex 1: Rouge
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Vertex 2: Vert
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f  // Vertex 3: Bleu
};

Ensuite, nous allons changer le vertex shader pour prendre en entrée la couleur et la passer au fragment shader :

#version 150

in vec2 position;
in vec3 color;

out vec3 Color;

void main()
{
Color = color;
gl_Position = vec4(position, 0.0, 1.0);
}

Et la variable Color est ajoutée comme entrée du fragment shader :

#version 150

in vec3 Color;

out vec4 outColor;

void main()
{
outColor = vec4(Color, 1.0);
}

Assurez-vous que la sortie du vertex shader et l'entrée du fragment shader ont le même nom, sinon les shaders ne vont pas pouvoir être liés.

Maintenant, nous devons simplement modifier le code lié aux attributs pour correspondre au nouvel ordre X, Y, R, G, B.

GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
5*sizeof(float), 0);

GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
5*sizeof(float), (void*)(2*sizeof(float)));

Le cinquième paramètre est maintenant défini à 5*sizeof(float), car chaque sommet se compose de cinq nombres à virgule flottante par valeur d'attribut. Le décalage pour la couleur est 2*sizeof(float), car nous devons passer les deux nombres à virgule flottante pour représenter la position.

Et c'est bon !

Vous devriez maintenant avoir une compréhension raisonnable des attributs de sommet et des shaders. Si vous rencontrez des problèmes, demandez sur le forum ou regardez ce code.

VII - Tampons d'éléments

Jusqu'à présent, les sommets sont spécifiés dans l'ordre dans lequel ils sont dessinés. Si vous vouliez ajouter un autre triangle, vous auriez ajouté trois sommets supplémentaires au tableau de sommets. Il y a un moyen pour contrôler l'ordre, nous permettant aussi de réutiliser des sommets existants. Cela peut économiser beaucoup de mémoire lorsque vous travaillez avec de vrais modèles 3D, car chaque point peut être un coin pour trois triangles !

Un tableau d'éléments est rempli avec des entiers non signés référençant les sommets liés à GL_ARRAY_BUFFER. Si nous souhaitons simplement les dessiner dans l'ordre présent, le tampon sera :

GLuint elements[] = {
0, 1, 2
};

Ils sont chargés en mémoire vidéo à travers un VBO, tout comme les données de sommets :

GLuint ebo;
glGenBuffers(1, &ebo);

...

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
sizeof(elements), elements, GL_STATIC_DRAW);

La seule chose qui change est la cible, qui, cette fois est GL_ELEMENT_ARRAY_BUFFER.

Pour utiliser ce tampon, vous devez changer la commande de dessin :

glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);

Le premier paramètre est le même que pour glDrawArrays (doc) mais les autres se rapportent au tampon d'éléments. Le deuxième paramètre indique le nombre d'indices à dessiner, le troisième le type de données des éléments et le dernier indique le décalage. La seule vraie différence est que vous parlez des indices au lieu des sommets.

Pour voir comment un tampon d'éléments peut être bénéfique, essayons de dessiner un rectangle en utilisant deux triangles. Nous allons commencer sans tampon d'éléments.

float vertices[] = {
-0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // Haut gauche
0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // Haut droit
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bas droit

0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bas droit
-0.5f, -0.5f, 1.0f, 1.0f, 1.0f, // Bas gauche
-0.5f,  0.5f, 1.0f, 0.0f, 0.0f  // Haut gauche
};

En appelant glDrawArrays (doc) à la place de glDrawElements (doc) comme précédemment, le tampon d'éléments est ignoré :

glDrawArrays(GL_TRIANGLES, 0, 6);

Le rectangle est affiché comme il se doit, mais la répétition des données de sommets est un gâchis de mémoire. L'utilisation d'un tampon d'éléments permet de réutiliser les données :

float vertices[] = {
-0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // Haut gauche
0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // Haut droit
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // Bas droit
-0.5f, -0.5f, 1.0f, 1.0f, 1.0f  // Bas gauche
};

...

GLuint elements[] = {
0, 1, 2,
2, 3, 0
};

...

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

Le tampon d'éléments indique toujours six sommets pour former deux triangles, mais maintenant nous pouvons réutiliser les sommets ! Cela peut ne pas sembler grand-chose, mais lorsque votre application graphique charge plusieurs modèles dans une mémoire graphique plutôt petite, les tampons d'éléments sont une optimisation considérable.

Si vous avez des problèmes, regardez le code source complet.

Ce chapitre a couvert tous les principes essentiels du dessin en OpenGL et il est absolument nécessaire que vous ayez une bonne compréhension de ceux-ci avant de continuer. Ainsi, je vous conseille de faire les exercices avant de plonger dans les textures.

VIII - Exercices

- Modifiez le vertex shader pour afficher le triangle de haut en bas. (Solution)

- Inversez les couleurs du triangle en modifiant le fragment shader. (Solution)

- Changez le programme afin que chaque sommet ne contienne qu'une valeur de couleur, définissant un niveau de gris. (Solution)

IX - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur open.gl.

Navigation

Tutoriel précédent : création du contexte

Sommaire

Tutoriel suivant : textures