Tutoriel 16 - Application basique de texture

Ogldev

Traducteur

Sylvain DOREMUS

Ce tutoriel présente le placage de textures.

Il nécessite l'installation d'ImageMagick.

Commentez

- Titre : Tutoriel 16 - Application basique de texture

- Auteur : Ogldev

- Traducteur : Sylvain DOREMUS

- Parution : 27 octobre 2014

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Etay Meiri. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Contexte

- II. Explication du code

- III. Exercice

- IV. Sources

- V. Remerciements

- Navigation

Navigation

Tutoriel précédent : contrôle de la caméra

Sommaire

Tutoriel suivant : éclairage ambiant

I - Contexte

L'application de texture consiste en l'application d'une image sur une ou plusieurs faces d'un modèle 3D. L'image (nommée « texture ») peut être n'importe quoi, mais représente généralement un motif, tel que des briques, du feuillage, de la terre, etc. Cela ajoute du réalisme à une scène. Comparons par exemple les images suivantes :

Pour que le placage de textures fonctionne, nous avons besoin de trois choses : charger la texture dans OpenGL, fournir les coordonnées de texture avec les sommets (pour appliquer la texture dessus) et effectuer une opération d'échantillonnage à partir de la texture, en utilisant les coordonnées de texture, pour récupérer la couleur du pixel. Comme un triangle est mis à l'échelle, tourné, déplacé et enfin projeté, il peut apparaître sur l'écran d'une multitude de façons possibles et sembler différent, selon son orientation par rapport à la caméra. Le GPU doit faire en sorte que la texture suive le mouvement des sommets du triangle afin que cela semble réaliste (si la texture semble « nager » en travers du triangle, ça ne sera pas bien). Pour cela, le développeur fournit un ensemble de coordonnées, connu sous le nom de « coordonnées de texture » pour chaque sommet. Quand le GPU dessine le triangle, il interpole les coordonnées de texture sur toute la face du triangle et, dans le fragment shader, le développeur affecte ces coordonnées à la texture. Cette action est nommée « échantillonnage » et son résultat est un texel (pixel dans la texture). Le texel contient souvent une couleur qui est utilisée pour peindre le pixel correspondant à l'écran. Dans les tutoriels à venir, nous verrons que le texel peut contenir différents types de données qui peuvent être utilisées pour divers effets.

OpenGL supporte divers types de texture tels que 1D, 2D, 3D, cube… qui peuvent être utilisés dans des techniques différentes. Nous nous contenterons d'étudier les textures 2D pour l'instant. Une texture 2D possède une largeur et une hauteur qui peuvent valoir n'importe quelle valeur, dans la limite de la spécification. La multiplication de la largeur par la hauteur nous donne le nombre de texels de la texture. Comment pouvons-nous spécifier les coordonnées de texture d'un sommet ? Non, ce ne sont pas les coordonnées du texel dans la texture. Ce serait trop limitant, car remplacer la texture par une autre ayant des dimensions différentes signifie que nous devrons mettre à jour les coordonnées de texture de chaque sommet pour qu'elles correspondent à la nouvelle texture. Le scénario idéal est d'être capable de changer de texture sans changer les coordonnées de texture. Ainsi, les coordonnées de texture sont exprimées dans « l'espace texture », qui est simplement l'intervalle normalisé [0,1]. Ce qui signifie que les coordonnées de texture utilisent généralement une fraction et, en multipliant cette fraction par la hauteur/largeur correspondante, on récupère les coordonnées du texel dans la texture. Par exemple, si les coordonnées de texture sont [0.5, 0.1] et que la texture a pour largeur 320 et pour hauteur 200, la position du texel sera (160, 20) (0.5 * 320 = 160 et 0.1 * 200 = 20).

La convention habituelle est d'utiliser U et V comme axes de l'espace texture, où U correspond à l'axe X du système cartésien de coordonnées 2D, et V correspond à l'axe Y. OpenGL traite les valeurs des axes UV, de gauche à droite pour l'axe U et de bas en haut pour l'axe V. Regardons l'image suivante :

Cette image représente l'espace texture et nous pouvons voir que l'origine de cet espace est le coin en bas à gauche. U augmente vers la droite et V augmente vers le haut. Considérons maintenant un triangle dont les coordonnées de texture sont définies comme sur l'image suivante :

Disons que nous appliquons une texture de sorte que lorsque nous utilisons ces coordonnées de texture, nous voyons l'image de la petite maison à l'emplacement ci-dessus. Maintenant le triangle subit diverses transformations et lorsque vient le moment de le rastériser, il ressemble à ceci :

Comme nous pouvons le voir, les coordonnées de texture « collent » aux sommets, car elles sont des attributs fondamentaux et ne changent pas avec les transformations. Lors de l'interpolation des coordonnées de texture, la plupart des pixels reçoivent la même coordonnée de texture que l'image originale (car ils sont restés à la même position, par rapport aux sommets) et, comme le triangle a été retourné, la texture a subi le même changement. Cela signifie que lorsque le triangle original est tourné, étiré ou écrasé, la texture suit sagement ces changements. Notons qu'il y a aussi des techniques, qui changent les coordonnées de texture, afin de déplacer la texture au travers de la face du triangle, de manière contrôlée. Cependant nos coordonnées resteront fixes pour l'instant.

Un autre concept important, associé au placage de texture est le « filtrage ». Nous avons discuté de comment associer les coordonnées de texture à un texel. La position du texel est toujours spécifiée en nombres entiers, mais qu'arrive-t-il lorsque les coordonnées de texture (qui sont, rappelons-le, des fractions entre 0 et 1) nous mappent un texel aux coordonnées (152.34, 745.14) ? La réponse triviale est de tronquer les valeurs pour obtenir (152, 745). Bien, cela fonctionne et produit des résultats adéquats, cependant, dans certains cas, cela n'est pas très joli. Une meilleure approche est de récupérer les texels en quadrilatères de deux sur deux ( (152, 745), (153, 745), (152, 746) et (153, 746) ), et de faire une interpolation linéaire entre leurs couleurs. Cette interpolation linéaire doit refléter la distance relative entre (152.34, 745.124) et chacun des autres texels. Plus la coordonnée est proche d'un texel, plus la participation de ce texel sur la couleur finale est importante, et plus elle est loin d'un texel, plus sa participation sera faible. Cela rend bien plus joli que l'approche initiale.

La méthode par laquelle la valeur du texel final est choisie est connue sous le nom de « filtrage ». La simple approche de troncation de la valeur est connue comme « filtrage au plus proche », et l'approche plus complexe que nous avons vue s'appelle « filtrage linéaire ». Un autre nom que vous pourrez rencontrer pour le filtre linéaire est « filtrage par point ». OpenGL supporte divers types de filtres, et vous avez la possibilité de choisir celui que vous voulez appliquer. Habituellement, les filtres qui fournissent les meilleurs résultats demandent plus de calculs au GPU, et peuvent donc avoir un effet sur la fréquence d'affichage. Le choix du type de filtrage est donc une question d'équilibrage, entre le résultat voulu et la capacité de la plate-forme cible.

Maintenant que nous comprenons le système de coordonnées de texture, il est temps d'étudier la manière dont le placage de texture est effectué avec OpenGL. L'application de texture signifie, dans OpenGL, la manipulation des connexions imbriquées de quatre concepts :

- la texture ;

- l'unité de texture ;

- l'objet d'échantillonnage ;

- la variable uniforme de l'échantillonneur dans le shader.

La texture contient les données de l'image elle-même (par exemple : les texels). La texture peut être de divers types (1D, 2D, etc.), avec diverses résolutions et le type de données sous-jacent peut être dans divers formats (RGB, RGBA, etc.). OpenGL fournit une manière de définir le point d'entrée des données en mémoire, ainsi que tous les attributs précédents et de charger ces données dans le GPU. Il y a aussi de multiples paramètres que nous pouvons contrôler, tels que le type de filtrage, etc. D'une manière très proche de celle utilisée pour les tampons de sommets, l'objet de texture est associé à un identifiant. Après la création de cet identifiant et le chargement des données de la texture ainsi que de ses paramètres, nous pouvons facilement échanger les textures, à la volée en liant les différents identifiants dans l'état OpenGL. Nous n'avons plus besoin de charger les données. À partir de maintenant, c'est au pilote OpenGL de s'assurer que les données sont chargées à temps sur le GPU, avant que le rendu ne commence.

La texture n'est pas liée directement dans le shader (où l'échantillonnage s'effectue réellement). À la place, elle est liée à une « unité de texture » dont l'index est donné au shader. Donc le shader accède à la texture au travers de l'unité de texture. Il y a généralement de multiples unités de texture disponibles, leur nombre exact dépendant des capacités de la carte graphique. Afin de lier une texture A à l'unité de texture 0 nous devons d'abord activer cette unité 0 puis lui lier la texture A. Nous pouvons maintenant activer l'unité de texture 1 pour lui lier une texture différente (ou la même d'ailleurs). L'unité de texture 0 reste liée à la texture A.

Il y a une petite astuce, dans le fait que chaque unité de texture peut être liée à plusieurs textures, tant que ces textures sont de types différents. C'est appelé la « cible » de la texture. Lorsque nous lions une texture à une unité de texture, nous spécifions la cible (1D, 2D, etc.). Nous pouvons donc avoir une texture A liée à la cible 1D alors que la texture B est liée à la cible 2D, et ce sur la même unité de texture.

L'opération d'échantillonnage a (généralement) lieu dans le fragment shader et il existe une fonction spéciale qui le fait. La fonction d'échantillonnage doit connaître l'unité de texture à laquelle elle doit accéder, car nous pouvons échantillonner plusieurs unités de texture dans un même fragment shader. Il y a un groupe de variables uniformes particulières pour cela, selon la cible de la texture : « sampler1d », « sampler2D », « sampler3D », « samplerCube », etc. Nous pouvons créer autant de variables uniformes d'échantillonnage que nous le souhaitons, et affecter à chacune la valeur d'une unité de texture, à partir de l'application. À chaque fois que nous appelons la fonction d'échantillonnage sur une variable uniforme d'échantillonnage, l'unité de texture correspondante (et la texture) sera utilisée.

Le dernier concept est l'objet d'échantillonnage, à ne pas confondre avec la variable uniforme d'échantillonnage ! Ce sont deux entités séparées. La texture contient aussi bien les données de la texture, que les paramètres qui configurent l'opération d'échantillonnage. Ces paramètres font partie de l'état d'échantillonnage. Cependant, nous pouvons créer un objet d'échantillonnage, le configurer avec un état d'échantillonnage, puis le lier à l'unité de texture. Lorsque nous faisons ainsi, l'objet d'échantillonnage remplace tous les états définis dans l'objet de texture. Pas d'inquiétude, pour l'instant nous n'utiliserons pas les objets d'échantillonnage, mais il est intéressant de savoir qu'ils existent.

Le digramme suivant résume les relations entre les différents concepts d'application de texture que nous venons d'apprendre :

II - Explication du code

OpenGL sait comment charger les données d'une texture de différents formats, à partir d'une adresse mémoire, mais ne fournit aucun moyen de charger la texture en mémoire, à partir de fichiers d'images tels que PNG ou JPG. Nous allons utiliser une bibliothèque externe à cette fin. Il y a de nombreuses options pour cela et nous allons utiliser ImageMagick, une bibliothèque libre, qui supporte de nombreux types d'images et est portable sur divers systèmes d'exploitation.

La majeure partie de la gestion de texture est encapsulée dans la classe suivante :

ogldev_texture.h

1. class Texture

2. {

3. public:

4. Texture(GLenum TextureTarget, const std::string& FileName);

6. bool Load();

8. void Bind(GLenum TextureUnit);

9. };

Lors de la création d'un objet Texture, nous devons définir une cible (nous utilisons GL_TEXTURE_2D) et un nom de fichier. Après cela, nous appelons la fonction Load(). Elle peut échouer, par exemple, si le fichier n'existe pas ou si ImageMagick a rencontré une erreur quelconque. Lorsque nous voulons utiliser une instance spécifique de Texture, nous devons la lier à une des unités de texture.

ogldev_texture.cpp

1. try {

2. m_pImage = new Magick::Image(m_fileName);

3. m_pImage->write(&m_blob, "RGBA");

4. }

5. catch (Magick::Error& Error) {

6. std::cout << "Error loading texture '" << m_fileName << "': " << Error.what() << std::endl;

7. return false;

8. }

Voici comment nous utilisons ImageMagick pour charger la texture depuis une image, puis la préparons en mémoire pour être chargée avec OpenGL. Nous commençons par charger un membre de classe dont le type est Magick::Image, en utilisant le nom de fichier de la texture. Cet appel charge la texture dans une représentation mémoire interne à ImageMagick et inutilisable directement par OpenGL. Ensuite nous écrivons l'image dans un objet de type Magick::Blob, en utilisant le format RVBA (rouge, vert, bleu et alpha). Le BLOB (« Binary Large Object ») est un mécanisme utile pour stocker l'image encodée en mémoire, de manière à pouvoir l'utiliser dans des programmes externes. S'il y a une erreur, une exception sera lancée, donc nous devons nous y préparer.

ogldev_texture.cpp

1. glGenTextures(1, &m_textureObj);

Cette fonction OpenGL est similaire à glGenBuffers()Gluint avec laquelle nous sommes déjà familiers. Elle génère le nombre demandé de textures et place leur identifiant dans le pointeur sur tableau de Gluint donné (le second paramètre). Dans notre cas, nous avons juste besoin d'une texture.

ogldev_texture.cpp

1. glBindTexture(m_textureTarget, m_textureObj);

Nous allons faire plusieurs appels relatifs aux textures et de manière similaire à ce que nous faisions déjà avec les tampons de sommets, nous devons dire à OpenGL sur quel objet il opère. C'est le but de la fonction glBindTexture()GL_TEXTURE_1DGLTEXTURE_2DGL_TEXTURE_2D. Elle dit à OpenGL quel est l'objet auquel nous nous référons dans tous les appels relatifs aux textures, jusqu'à ce qu'une nouvelle texture soit liée. En plus de l'identifiant (second paramètre), nous spécifions aussi la cible de la texture, qui peut valoir GL_TEXTURE_1D, GLTEXTURE_2D, etc. Il peut y avoir une texture différente liée à chacune des cibles simultanément. Dans notre implémentation, la cible est donnée en tant que paramètre du constructeur (et pour l'instant nous utilisons GL_TEXTURE_2D).

ogldev_texture.cpp

1. glTexImage2D(m_textureTarget, 0, GL_RGBA, m_pImage->columns(), m_pImage->rows(), 0, GL_RGBA, GL_UNSIGNED_BYTE, m_blob.data());

Cette fonction, assez complexe, est utilisée afin de charger la partie principale de la texture, c'est-à-dire les données elles-mêmes. Il y a plusieurs fonctions glTexImage* disponibles, chacune couvrant quelques types de cibles de texture. La cible de la texture est toujours le premier paramètre. Le second paramètre est le LOD ou « Level Of Detail ». Une texture peut contenir la même image dans des résolutions différentes, un concept connu sous le nom de « mip-mapping ». Chaque « mip-map » possède un index de LOD différent, en partant de 0 jusqu'à la résolution la plus haute, croissant alors que la résolution décroit. Pour l'instant nous avons un seul « mip-map » donc nous donnons zéro.

Le paramètre suivant est le format interne dans lequel OpenGL stocke la texture. Par exemple, nous donner une texture avec le panel de couleurs complet (rouge, vert, bleu et alpha), mais si nous spécifions GL_RED, nous aurons une texture avec juste le canal rouge, ce qui apparaîtra… rouge (essayez !). Nous utilisons GL_RGBA pour récupérer toutes les couleurs de la texture correctement. Les deux paramètres suivants sont la largeur et la hauteur de la texture, en texels. La bibliothèque ImageMagick stocke gentiment ces informations pour nous lorsqu'elle charge l'image, nous pouvons les récupérer en utilisant les fonctions Image::columns()/rows(). Le cinquième paramètre est la bordure, que nous laissons à zéro pour le moment.

Les trois derniers paramètres spécifient la source des données en entrée. Les paramètres sont le format, le type et l'adresse mémoire. Le format nous indique le nombre de canaux et doit correspondre au BLOB que nous avons en mémoire. Le type décrit le type de données que nous avons par canal. OpenGL supporte de nombreux types de données, mais le BLOB ImageMagick définit un octet par canal, donc nous utilisons GL_UNSIGNED_BYTE. Enfin vient l'adresse mémoire des données que nous extrayons depuis le BLOB, en utilisant la fonction Blob::data().

ogldev_texture.cpp

1. glTexParameterf(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

2. glTexParameterf(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

La fonction générale glTexParameterf() contrôle de nombreux aspects de l'opération d'échantillonnage. Ces aspects font partie de l'état d'échantillonnage de la texture. Ici nous spécifions le filtre à utiliser pour le grossissement et le rétrécissement. Chaque texture possède une largeur et une hauteur, mais celles-ci sont rarement appliquées à un triangle dans les mêmes proportions. Dans la plupart des cas le triangle est soit plus petit soit plus grand que la texture. Dans ces cas le type de filtre détermine comment gérer le cas du grossissement ou du rétrécissement de la texture pour qu'elle corresponde aux proportions du triangle. Quand le triangle rastérisé est plus grand que la texture (par exemple : très proche de la caméra), nous pouvons avoir plusieurs pixels couverts par le même texel (grossissement). Quand il est plus petit (par exemple : très loin de la caméra), plusieurs texels peuvent être couverts par le même pixel (rétrécissement). Ici nous choisissons le filtre d'interpolation linéaire pour les deux cas. Comme nous l'avons vu précédemment, l'interpolation linéaire fournit des résultats ayant un bon rendu, en mélangeant les couleurs d'un carré de 2x2 texels, basé sur la proximité du texel actuel (calculé en multipliant les coordonnées de texture par les dimensions de la texture).

ogldev_texture.cpp

1. void Texture::Bind(GLenum TextureUnit)

2. {

3. glActiveTexture(TextureUnit);

4. glBindTexture(m_textureTarget, m_textureObj);

5. }

Comme notre application 3D devient plus complexe, nous pouvons utiliser plusieurs textures différentes dans divers appels de dessin dans la boucle de rendu. Avant chaque appel à la fonction de dessin, nous devons lier la texture que nous voulons à une des unités de texture afin qu'elle soit échantillonnée dans le fragment shader. Cette fonction prend l'index de l'unité de texture (GL_TEXTURE0, GL_TEXTURE1, etc.) comme paramètre. Elle l'active en utilisant la fonction glActiveTexture()Texture::Bind() puis lui lie la texture. Cette texture restera liée à cette unité de texture jusqu'au prochain appel de Texture::Bind() avec la même unité de texture.

shader.vs

1. #version 330

3. layout (location = 0) in vec3 Position;

4. layout (location = 1) in vec2 TexCoord;

6. uniform mat4 gWVP;

8. out vec2 TexCoord0;

10. void main()

11. {

12. gl_Position = gWVP * vec4(Position, 1.0);

13. TexCoord0 = TexCoord;

14. };

Ceci est le code du vertex shader mis à jour. Il y a un paramètre additionnel, nommé TexCoord qui est un vecteur 2D. Au lieu de définir la couleur de sortie, ce shader transfère les coordonnées de texture vers le fragment shader, sans y toucher. La rastérisationva effectuer une interpolation des coordonnées de texture à travers la face du triangle et chaque fragment shader sera appelé avec ses propres coordonnées de texture.

shader.fs

1. in vec2 TexCoord0;

3. out vec4 FragColor;

5. uniform sampler2D gSampler;

7. void main()

8. {

9. FragColor = texture2D(gSampler, TexCoord0.st);

10. };

Et voici le code du fragment shader mis à jour. Il possède une variable en entrée appelée TexCoord0 qui contient les coordonnées de texture interpolées, que nous récupérons depuis le vertex shader. On trouve une variable uniforme nommée gSampler qui a comme type sampler2D. L'application doit définir comme valeur pour cette variable l'index de l'unité de texture, afin que le fragment shader soit capable d'accéder à la texture. La fonction principale fait une seule chose : elle utilise la fonction interne texture2D pour échantillonner la texture. Le premier paramètre est la variable uniforme de l'échantillonneur et le second paramètre reçoit les coordonnées de texture. La valeur retournée est le texel échantillonné (qui dans notre cas contient une couleur), après avoir été filtré. C'est la couleur finale du pixel dans ce tutoriel. Dans les prochains tutoriels, nous verrons que l'éclairage change simplement cette couleur, en se basant sur les paramètres des sources lumineuses.

1. Vertex Vertices[4] = {

2. Vertex(Vector3f(-1.0f, -1.0f, 0.5773f), Vector2f(0.0f, 0.0f)),

3. Vertex(Vector3f(0.0f, -1.0f, -1.15475), Vector2f(0.5f, 0.0f)),

4. Vertex(Vector3f(1.0f, -1.0f, 0.5773f), Vector2f(1.0f, 0.0f)),

5. Vertex(Vector3f(0.0f, 1.0f, 0.0f), Vector2f(0.5f, 1.0f)) };

Jusqu'à ce tutoriel, notre tampon de sommets était une simple suite de Vector3f contenant les positions. Maintenant nous avons la structure Vertex qui contient aussi les coordonnées de texture en tant que Vector2f.

tutorial16.cpp

1. ...

2. glEnableVertexAttribArray(1);

3. ...

4. glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);

5. glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)12);

6. ...

7. pTexture->Bind(GL_TEXTURE0);

8. ...

9. glDisableVertexAttribArray(1);

Voici les changements et ajouts effectués au niveau de la boucle de rendu. Nous commençons par activer l'attribut de sommet 1 pour les coordonnées de texture, en plus de l'attribut 0 qui est déjà activé pour la position. Cela correspond à la disposition des déclarations dans le vertex shader. Ensuite nous appelons glVertexAttribPointerVertexsizeof( Vector3f )VertexVertexGlvoid* pour définir la position des coordonnées de texture dans le tampon de sommets. Une coordonnée de texture est composée de deux valeurs en nombre flottant, ce qui correspond aux deuxième et troisième paramètres. Regardons le cinquième paramètre. Il s'agit de la taille de la structure Vertex et il est spécifié pour la position et les coordonnées de texture. Ce paramètre est connu sous le nom de « pas » et dit à OpenGL le nombre d'octets entre le début d'un attribut d'un sommet et le début du même attribut dans le sommet suivant. Dans notre cas, le tampon contient : position0, texture0, position1, texture1, etc. Dans les tutoriels précédents, nous avions uniquement la position dans le tampon, nous pouvions donc sans soucis mettre ce paramètre à zéro ou sizeof( Vector3f ). Maintenant que nous avons plus d'un attribut, le pas peut uniquement valoir le nombre d'octets de la structure Vertex. Le dernier paramètre est le décalage en octets, depuis le début de la structure Vertex, jusqu'aux attributs de texture. Nous devons faire la conversion en Glvoid*, car c'est ainsi que la fonction attend de recevoir ce décalage.

Avant l'appel à la fonction de dessin, nous devons lier la texture que nous voulons utiliser à une unité de texture. Nous avons une seule texture donc n'importe quelle unité suffirait. Nous avons seulement besoin de nous assurer que la même unité de texture est définie dans le shader (voir plus bas). Après l'appel à la fonction de dessin, nous désactivons l'attribut.

tutorial16.cpp

1. glFrontFace(GL_CW);

2. glCullFace(GL_BACK);

3. glEnable(GL_CULL_FACE);

Ces appels OpenGL ne sont pas vraiment relatifs à l'application de texture, je les ai juste ajoutés afin que cela rende plus joli (essayez de les désactiver…). Ils activent l'élimination des faces cachées, une optimisation commune, utilisée afin d'ignorer des triangles avant le lourd procédé de rastérisation. La motivation ici est qu'il y a souvent 50 % de la surface d'un objet qui nous est cachée (le dos d'une personne, maison, voiture, etc.). La fonction glFrontFace()() dit à OpenGL, que les sommets du tampon de sommets sont ordonnés dans le sens des aiguilles d'une montre. C'est-à-dire, lorsque nous regardons la face avant du triangle, nous trouvons les sommets dans le tampon rangés dans le sens des aiguilles d'une montre. La fonction glCullFace() dit au GPU d'ignorer les triangles à l'arrière du triangle. Cela signifie que l'intérieur d'un objet n'a pas besoin d'être rendu, seulement l'extérieur. Enfin l'élimination des faces arrière en elle-même est activée (par défaut elle est désactivée). Notons que, dans ce tutoriel, j'ai inversé l'ordre des index dans le triangle du bas. La manière dont c'était fait auparavant faisait apparaître le triangle comme s'il était tourné vers l'intérieur de la pyramide (ligne 170 de tutorial16.cpp).

tutorial16.cpp

1. glUniform1i(gSampler, 0);

Ici nous définissons l'index de l'unité de texture que nous allons utiliser, dans la variable uniforme d'échantillonnage du shader. gSampler est une variable uniforme dont la valeur a été récupérée auparavant, en utilisant la fonction glGetUniformLocation()GL_TEXTURE0. Il est important de noter que l'index effectif de l'unité de texture est utilisé ici et pas l'énumération GL_TEXTURE0 (qui a une valeur différente).

tutorial16.cpp

1. pTexture = new Texture(GL_TEXTURE_2D, "test.png");

3. if (!pTexture->Load()) {

4. return 1;

5. }

Ici nous créons l'objet Texture et le chargeons. « test.png » est inclus avec les sources de ce tutoriel, mais ImageMagick devrait être capable de gérer presque n'importe quel fichier que nous y mettrions.

III - Exercice

Si vous exécutez le code fourni avec ce tutoriel, vous remarquerez que les faces de la pyramide ne sont pas identiques. Essayez de comprendre ce qu'il se passe et ce qui devrait être modifié pour les rendre identiques.

IV - Sources

Vous pouvez télécharger les sources de ce projet en suivant ce lien :

Récupérer les sources

V - Remerciements

Merci à Etay Meiri de nous permettre de traduire son tutoriel.

Merci à LittleWhite pour ses corrections, à kimz et Claude LELOUP pour leur relecture orthographique.

Navigation

Tutoriel précédent : contrôle de la caméra

Sommaire

Tutoriel suivant : éclairage ambiant