OpenGL

Textures

Alexander Overvoorde

Traducteur

Alexandre Laurent

- Titre : OpenGL

- Auteur : Alexander Overvoorde

- Traducteur : Alexandre Laurent

- Parution : 2 décembre 2015

- 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. Objets textures et paramètres

- I-A. Wrapping

- I-B. Filtrage

- II. Chargement des images de texture

- II-A. SOIL

- II-B. Alternatives

- III. Utiliser une texture

- IV. Unités de texture

- V. Exercices

- VI. Remerciements

- Navigation

Navigation

Tutoriel précédent : dessiner des polygones

Sommaire

Tutoriel suivant : Transformations

I - Objets textures et paramètres

Tout comme pour les VBO et les VAO, les textures sont des objets qui doivent être créés en appelant une fonction. Le nom de celle-ci ne devrait pas être surprenant.

GLuint tex;
glGenTextures(1, &tex);

Les textures sont évidemment utilisées comme images pour décorer les modèles 3D, mais en réalité, elles peuvent être utilisées pour stocker bien d’autres sortes de données. Il est possible d'avoir des textures en 1D, 2D et même des textures 3D, pouvant être utilisées pour stocker un volume de données sur le GPU. Un autre exemple d'utilisation des textures est le stockage des informations sur le terrain. Dans cet article nous nous intéresserons aux textures pour les images, mais les principes s'appliquent généralement à tous les types de textures.

glBindTexture(GL_TEXTURE_2D, tex);

Tout comme les autres objets, les textures doivent être liées pour effectuer des opérations sur celles-ci. Sachant que les images sont des tableaux 2D de pixels, la texture va être liée à la cible GL_TEXTURE_2D.

Les pixels dans la texture vont être récupérés en utilisant des coordonnées de texture pendant les opérations d'affichage. Ces coordonnées sont comprises entre 0.0 et 1.0 où (0, 0) est, par convention, le coin bas gauche et (1, 1) le coin haut droit de l'image de la texture. L'opération qui utilise ces coordonnées de texture pour récupérer l'information de couleur à partir des pixels est appelée échantillonnage (« sampling »). Il y a différentes manières d'aborder ce problème, chacune adéquate pour tel ou tel scénario. OpenGL offre plusieurs options pour contrôler comment l’échantillonnage est effectué. Les plus courantes seront abordées ici.

I-A - Wrapping

La première chose que vous devez prendre en considération est la façon dont la texture est échantillonnée lorsque la coordonnée de texture n'est pas comprise entre 0 et 1. OpenGL apporte quatre solutions à cela :

- GL_REPEAT : la partie entière de la coordonnée sera ignorée, ce qui donne une répétition de la texture ;

- GL_MIRRORED_REPEAT : la texture sera aussi répétée, mais inversée lorsque la partie entière de la coordonnée est impaire ;

- GL_CLAMP_TO_EDGE : la coordonnée sera simplement réduite entre 0 et 1 ;

- GL_CLAMP_TO_BORDER : la coordonnée qui n'est pas comprise entre 0 et 1 obtiendra une couleur spécifique de la bordure.

Ces explications peuvent être un peu énigmatiques et comme OpenGL n'est que graphisme, voyons le rendu de chacun de ces cas :

Ce comportement peut être définie pour chaque coordonnée (s, t, r), l'équivalent des (x, y, z) pour les textures. Le paramètre d'une texture peut être changé avec les fonctions glTexParameter* comme suit :

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

Comme précédemment, le 'i' dans le nom de la fonction indique la valeur que vous souhaitez spécifier. Si vous utilisez GL_CLAMP_TO_BORDER et que vous voulez changer la couleur de la bordure, vous devez changer la couleur de GL_TEXTURE_BORDER_COLOR en lui donnant un tableau de flottants représentant les valeurs RGBA :

float color[] = { 1.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, color);

Cette opération va définir la bordure à la couleur rouge.

I-B - Filtrage

Comme les coordonnées de texture sont indépendantes de la résolution, elles ne vont pas toujours parfaitement correspondre à un pixel. Cela arrive lorsqu'une image de texture est étirée ou réduite par rapport à sa taille originelle. OpenGL offre différentes méthodes pour décider de la couleur échantillonnée lorsque cela arrive. Ce processus est appelé filtrage et voici les différentes méthodes disponibles :

- GL_NEAREST : retourne le pixel le plus proche des coordonnées ;

- GL_LINEAR : retourne la moyenne pondérée des quatre pixels entourant les coordonnées fournies ;

- GL_NEAREST_MPIMAP_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR : échantillonnent à partir des mipmaps.

Avant de parler des mipmaps, voyons la différence entre récupérer le plus proche pixel et faire une interpolation linéaire. L'image d'origine est 16 fois plus petite que le rectangle sur lequel elle est appliquée.

Bien que l'interpolation linéaire donne un résultat plus doux, ce n'est pas toujours l'option idéale. L'interpolation du voisin le plus proche est plus appropriée pour les jeux voulant reproduire des graphismes 8 bits, afin d’avoir un aspect pixelisé.

Vous pouvez spécifier le type d'interpolation à utiliser dans deux cas distincts : la réduction et l'agrandissement de l'image. Ces deux cas sont identifiés par les mots clés GL_TEXTURE_MIN_FILTER et GL_TEXTURE_MAG_FILTER.

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

Comme vous pouvez le voir, il y a une autre méthode pour filtrer les textures : les mipmaps. Les mipmaps sont des copies réduites de votre texture dont le calcul et le filtrage ont été effectués à l'avance. Il est recommandé de les utiliser, car elles permettent d'obtenir une meilleure qualité et de meilleures performances.

glGenerateMipmap(GL_TEXTURE_2D);

Les générer est aussi simple que d'appeler la fonction ci-dessus, donc il n'y a aucune excuse pour ne pas les utiliser ! Mais vous devez charger l'image de la texture avant que les mipmaps puissent être générées.

Pour utiliser les mipmaps, choisissez une des quatre méthodes de filtrage de mipmap :

- GL_NEAREST_MIPMAP_NEAREST : utilise la mipmap correspondant le mieux à la taille du pixel sur lequel la texture est appliquée et échantillonne en utilisant l'interpolation du plus proche voisin ;

- GL_LINEAR_MIPMAP_NEAREST : échantillonne la mipmap la plus proche avec l'interpolation linéaire ;

- GL_NEAREST_MIPMAP_LINEAR : utilise les deux mipmaps les plus proches de la taille du pixel sur lequel la texture est appliquée et effectue l'échantillonnage avec l'interpolation du plus proche voisin ;

- GL_LINEAR_MIPMAP_LINEAR : échantillonne les deux mipmaps les plus proches avec l'interpolation linéaire.

D'autres paramètres de texture sont disponibles, mais ils sont conçus pour des opérations spécifiques. Vous pouvez en apprendre plus dans la spécification.

II - Chargement des images de texture

Maintenant que l'objet de texture a été configuré, il est temps de charger l'image. Cela s'effectue simplement en chargeant un tableau de pixels dans celui-ci :

// Échiquier blanc/noir
float pixels[] = {
0.0f, 0.0f, 0.0f,   1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f,   0.0f, 0.0f, 0.0f
};
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_FLOAT, pixels);

Le premier paramètre après la texture cible est le niveau de détail, où 0 est l'image de base. Ce paramètre peut être utilisé pour charger vos mipmaps. Le second paramètre indique le format interne des pixels, le format dans lequel vous souhaitez que les pixels soient stockés dans la carte graphique. Plusieurs formats sont disponibles, ainsi que des formats d'images compressées, donc cela peut être intéressant de regarder toutes les options disponibles. Les troisième et quatrième paramètres indiquent la largeur et la hauteur de l'image. Le cinquième paramètre doit toujours avoir une valeur de 0 d'après la spécification. Les deux paramètres qui suivent décrivent le format des pixels dans le tableau qui sera chargé et le dernier paramètre indique le tableau lui-même. La fonction débute par le chargement aux coordonnées (0, 0), prenez garde à cela.

Mais comment le tableau de pixels est-il défini ? Les textures dans les applications graphiques vont généralement être plus sophistiquées que de simples motifs et seront chargées à partir de fichiers. La meilleure habitude est d'avoir vos fichiers dans un format supporté par le matériel, mais cela peut être plus pratique de charger les textures à partir d'images JPG et PNG. Malheureusement, OpenGL n'offre aucune fonction pour charger les pixels de ces formats de fichier, mais c'est pour cela que les bibliothèques tierces sont utiles. La bibliothèque SOIL va être décrite ci-dessous ainsi que des alternatives à celle-ci.

II-A - SOIL

SOIL (Simple OpenGL Image Library) est une petite bibliothèque facile à utiliser qui charge les fichiers images directement en objet de texture ou qui en crée pour vous. Vous pouvez commencer à l'utiliser dans votre projet en le liant avec SOIL et en ajoutant le répertoire src dans vos chemins d'inclusion. La bibliothèque inclut des fichiers de projet Visual Studio pour la compiler vous-même.

Bien que SOIL apporte des fonctions pour créer automatiquement une texture à partir d'une image, elle utilise des fonctionnalités qui ne sont pas disponibles en OpenGL moderne. À cause de cela, nous allons simplement utiliser SOIL pour charger des images et créer les textures nous-même.

int width, height;
unsigned char* image =
SOIL_load_image("img.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);

Vous pouvez démarrer la configuration des paramètres de la texture et générer les mipmaps après ça.

SOIL_free_image_data(image);

Vous pouvez nettoyer les données de l'image juste après l’avoir chargée dans la texture.

Comme indiqué précédemment, OpenGL s'attend à ce que le premier pixel soit situé dans le coin bas gauche, ce qui signifie que les textures seront retournées horizontalement après avoir été chargées par SOIL. Pour parer cela, le code de ce tutoriel utilisera des coordonnées Y inversées pour les coordonnées de texture à partir de maintenant. Cela signifie que 0 est supposé être le coin supérieur gauche au lieu du coin inférieur gauche. Cette technique peut, par effet de bord, rendre les coordonnées de texture plus intuitives.

II-B - Alternatives

Les autres bibliothèques supportant un large panel de formats de fichier comme SOIL sont DevIL et FreeImage. Si vous ne vous intéressez qu'à un seul type de fichier, il est aussi possible d'utiliser des bibliothèques comme libpng et libjpeg directement. Si vous aimez les défis, regardez la documentation des formats BMP et TGA. Ce n'est pas si dur d'implémenter le chargement par vous-même.

III - Utiliser une texture

Comme vous avez vu, les textures sont échantillonnées en utilisant des coordonnées de texture et vous devez ajouter celles-ci comme attributs de vos sommets. Modifions le dernier exemple du chapitre précédent pour inclure ces coordonnées de texture. Le nouveau tableau de sommets contient les coordonnées s et t pour chaque sommet.

float vertices[] = {
//  Position      Couleur             Coordonnées de texture
-0.5f,  0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, // Haut gauche
0.5f,  0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Haut droit
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, // Bas droit
-0.5f, -0.5f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f  // Bas gauche
};

Le vertex shader doit être modifié afin que les coordonnées de texture soient interpolées pour les fragments :

...

in vec2 texcoord;

out vec3 Color;
out vec2 Texcoord;

...

void main()
{
Texcoord = texcoord;

Tout comme pour les attributs de couleur, le pointeur vers les attributs doit être adapté au nouveau format :

glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
7*sizeof(float), 0);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
7*sizeof(float), (void*)(2*sizeof(float)));

GLint texAttrib = glGetAttribLocation(shaderProgram, "texcoord");
glEnableVertexAttribArray(texAttrib);
glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE,
7*sizeof(float), (void*)(5*sizeof(float)));

Sachant que deux floats ont été ajoutés pour les coordonnées, un sommet contient maintenant 7 floats et l'attribut des coordonnées de texture correspond à deux de ces floats.

Maintenant il ne reste plus qu'une chose à faire : fournir un accès à la texture dans le fragment shader pour échantillonner les pixels à partir de celle-ci. Cela se fait en ajoutant une variable uniforme du type sampler2D, qui va avoir une valeur par défaut de 0. Cette valeur doit être modifiée uniquement lorsque nous devons accéder à plusieurs textures, ce que nous verrons dans la prochaine partie.

Pour cet exemple, l'image du chaton utilisée ci-dessus sera chargée avec la bibliothèque SOIL. Assurez-vous qu'elle se trouve dans le répertoire de travail de l'application.

int width, height;
unsigned char* image =
SOIL_load_image("sample.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);

Pour échantillonner un pixel à partir de la texture 2D en utilisant l'échantillonneur, la fonction texture est appelée avec l'échantillonneur adéquat et les coordonnées de texture. Nous allons aussi multiplier la couleur échantillonnée avec la couleur de l'attribut pour obtenir un effet intéressant. Votre fragment shader doit maintenant ressembler à :

#version 150

in vec3 Color;
in vec2 Texcoord;

out vec4 outColor;

uniform sampler2D tex;

void main()
{
outColor = texture(tex, Texcoord) * vec4(Color, 1.0);
}

Lors de l'exécution de cette application, vous devriez obtenir le résultat suivant :

Si vous obtenez un écran noir, assurez-vous que vos shaders ont été compilés avec succès et que l'image est correctement chargée. Si vous ne pouvez pas trouver le problème, essayez de comparer votre code avec celui de l'exemple.

IV - Unités de texture

L'échantillonneur de votre fragment shader est lié à l'unité de texture 0. Les unités de texture sont des références pour les objets de texture qui peuvent être échantillonnés dans un shader. Les textures sont liées aux unités de texture avec la fonction glBindTexture (doc) que vous avez utilisée précédemment. Comme vous n'avez pas spécifié explicitement quelle unité de texture utiliser, la texture a été liée à GL_TEXTURE0. C'est pourquoi la valeur par défaut 0 pour l'échantillonneur dans votre shader fonctionne.

La fonction glActiveTexture (doc) indique à quelle unité de texture un objet de texture est lié lorsque glBindTexture (doc) est appelé.

glActiveTexture(GL_TEXTURE0);

Le nombre d'unités de texture supportées n'est pas le même pour toutes les cartes graphiques, mais sera au minimum de 48. Il est prudent de dire que vous ne devez jamais atteindre cette limite et cela même dans les applications graphiques les plus extrêmes.

Pour nous entraîner avec l'échantillonnage à partir de multiples textures, essayons de fusionner l'image du chaton avec celle d'un chiot afin d'obtenir le meilleur des deux mondes ! Modifions le fragment shader pour échantillonner deux textures et fusionner les pixels :

...

uniform sampler2D texKitten;
uniform sampler2D texPuppy;

void main()
{
vec4 colKitten = texture(texKitten, Texcoord);
vec4 colPuppy = texture(texPuppy, Texcoord);
outColor = mix(colKitten, colPuppy, 0.5);
}

La fonction mix est une fonction GLSL qui effectue une interpolation linéaire entre deux variables suivant son troisième paramètre. Une valeur de 0.0 donnera la première valeur, une valeur de 1.0 donnera la seconde valeur et une valeur intermédiaire donnera un mélange des deux valeurs. Vous allez avoir la chance de tester cela dans les exercices.

Maintenant que les deux échantillonneurs sont prêts, vous devez assigner les deux premières unités de texture à ceux-ci et lier les deux textures à ces unités. Cela se fait en ajoutant les appels adéquats à la fonction glActiveTexture (doc) dans le code de chargement des textures.

GLuint textures[2];
glGenTextures(2, textures);

int width, height;
unsigned char* image;

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
image = SOIL_load_image("sample.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
glUniform1i(glGetUniformLocation(shaderProgram, "texKitten"), 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textures[1]);
image = SOIL_load_image("sample2.png", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, image);
SOIL_free_image_data(image);
glUniform1i(glGetUniformLocation(shaderProgram, "texPuppy"), 1);

Les unités de texture des échantillonneurs sont définies par la fonction glUniform que vous avez vue dans le chapitre précédent. La fonction accepte simplement un entier spécifiant l'unité de texture. Ce code devrait afficher l'image suivante :

Comme toujours, regardez le code de l'exemple si vous avez un problème pour réussir.

Maintenant que l'échantillonnage de texture a été présenté dans ce chapitre, vous êtes finalement prêt à plonger dans les transformations et arriver à la 3D. La connaissance que vous avez acquise devrait suffire pour produire la plupart des jeux 2D, sauf ceux utilisant des transformations comme la rotation et le redimensionnement, qui seront étudiées dans le prochain chapitre.

V - Exercices

- Animer le mélange entre les deux textures en ajoutant une variable uniforme time. (Solution)

- Dessiner une réflexion du chaton dans la moitié inférieure du rectangle. (Solution)

- Maintenant, essayer d'ajouter une distorsion avec sin et la variable time pour simuler un effet d'eau. (Résultat attendu, Solution)

VI - Remerciements

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

Navigation

Tutoriel précédent : dessiner des polygones

Sommaire

Tutoriel suivant : Transformations