TD4 : Textures

Voilà les étapes nécessaires à la création d'une texture en OpenGL :

  1. obtention d'un identifiant de texture (glGenTextures)
  2. lien avec cette texture (glBindTexture)
  3. spécification des paramètres (glTexParameter), notamment de filtrage et de répétition/pavage
  4. chargement de l'image depuis un fichier
  5. envoie des dimensions et des texels (glTexImage2D)
  6. génération des niveaux de mipmaps (glGenerateMipmap)

En pratique, vous utiliserez la bibliothèque SOIL2 (déjà intégrée au projet) qui permet de réaliser ces opérations en un appel de fonction (SOIL_load_OGL_texture).

À l'affichage, il faut spécifier sur quelle unité de texture on veut attacher la texture (glActiveTexture) en la liant avec l'identifiant de texture correspondant (glBindTexture). Dans les shaders, il est alors possible de manipuler les unités de textures auxquelles on a associées un nom par le passage d'un uniform (uniform sampler2D Tex2D prend la valeur 0 pour accéder à l'unité 0, par exemple).

Bien sûr, il faut également fournir les coordonnées de texture de l'objet comme attributs pour pouvoir accéder au sampler par la fonction GLSL texture. Pour la première partie de ce TD, vous utiliserez une sphère paramétrique (classe Sphere) pour laquelle il est simple de calculer des coordonnées de texture (latitude et longitude).

1 - Texture de couleurs

1. Dans Viewer::init, Chargez "earth1.png" (depuis le répertoire "data/textures") dans une texture OpenGL à l'aide de SOIL. Créez un shader affichant cette texture à la surface de la sphère. Pour cela, vous devrez passer les coordonnées de texture in vec2 vtx_texcoord du vertex au fragment shader, puis les utiliser pour accéder à un sampler2D (déclaré comme variable uniforme) avec la fonction GLSL texture.

terre
Globe terrestre de jour, de nuit et mélange entre les deux selon l'éclairage

2. Pour simuler l'éclairage par le soleil, ajoutez la contribution d'une source lumineuse directionnelle. Comme dans le TD3, calculez un coefficient diffus qui fera varier la couleur finale entre celle de la texture et le noir.

3. Chargez la deuxième texture "earth2.png" – correspondant à la terre vue de nuit – dans une deuxième unité de texture. Modifiez le code de votre fragment shader pour remplacer le noir par cette texture (vous pouvez utiliser la fonction GLSL mix).

2 - Filtrage

1. Ajoutez les attributs suivant à la classe Viewer :
GLuint _samplerId;
GLenum _minFilter = GL_NEAREST;
GLenum _magFilter = GL_NEAREST;

Dans la fonction Viewer::init, créez un sampler object (cf. glGenSamplers) et initialisez ses paramètres de filtrage à _minFilter et _magFilter. Lors de l'affichage, activez ce sampler et redéfinissez systématiquement ses paramètres de filtrage : ainsi, si ceux-ci sont modifiés, ils seront automatiquement mis à jour. Les paramètre du sampler sont utilisés à la place de ceux des textures lorsqu'un sampler est lié, par conséquent toutes vos textures seront filtrées avec les paramètres _minFilter et _magFilter.

checkerboard

Dans Viewer::keyPressed, assignez des touches pour modifier les variables _minFilter et _magFilter de façon à pouvoir tester toutes les combinaisons de filtres. La magnification accepte les paramètres GL_NEAREST et GL_LINEAR, et la mingnification accepte en plus les 4 valeurs GL_*_MIPMAP_*, où * est remplacé par GL_NEAREST ou GL_LINEAR.

En jouant sur le zoom, observer la qualité obtenue avec les différents modes de filtrage. Testez avec la sphere et le maillage "cow.obj". Vous pouvez aussi tester avec un plan (classe Quad), ainsi qu'avec d'autres textures, comme "checkerboard.png".

2. Utilisez le code suivant dans Viewer::init pour générer procéduralement une texture et sa mip-map :

int w = 256;
int h = 256;
unsigned char colors[6][3] = { {255,  0 , 0}, {0, 255,  0 }, { 0,  0, 255}, 
                               {255, 255, 0}, {0, 255, 255}, {255, 0, 255} };
unsigned char data[w*h*3];
glGenTextures(1,&mTexID);
glBindTexture(GL_TEXTURE_2D, mTexID);
for(int l=0; ; ++l) {
    for(int i=0; i<w*h; ++i){
        data[3*i  ] = colors[l][0];
        data[3*i+1] = colors[l][1];
        data[3*i+2] = colors[l][2];
    }
    glTexImage2D(GL_TEXTURE_2D, l, (GLint)GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    if(w==1 && h==1)
      break;
    w = std::max(1, w/2);
    h = std::max(1, h/2);
}

Ce code charge dans chaque niveau de mip-map une image de couleur unie, alternativement rouge, vert, bleu, jaune, cyan et magenta. Prenez un peu de temps pour comprendre le fonctionnement de ce code.

Maintenant, re-testez les différents modes de filtrage sur plusieurs modèles avec cette texture. Vous pouvez éventuellement désigner une touche pour passer d'une texture standard à cette textures spéciale. Qu'en concluez-vous sur le fonctionnement du mipmapping ?

Important : les niveaux de mip-maps étant générés à la main, il ne faut pas appeler glGenerateMipmap() ou utiliser le flag SOIL_FLAG_MIPMAPS pour que cet exercice fonctionne.

mipmap

3 - Normal Mapping

Nous allons ajouter du relief au globe par la technique de normal mapping. Le principe de cette approche est de stocker dans une texture une carte des normales de l'objet.

1. Commencez par charger la texture "earth3.png" dans une troisième unité de texture. Vous pouvez dans un premier temps visualiser directement cette texture à la surface de la sphère.

2. Les normales fournies par la texture sont exprimées dans un repère local, tangent à la surface de l'objet. Pour les utiliser, il est donc nécessaire d'exprimer les vecteurs de lumière et de vue dans ce repère ($l_t$ et $v_t$). Ajoutez au vertex shader l'attribut vec3 vtx_tangent pour avoir accès à la tangente en chaque sommet, puis construisez ce repère en suivant les étapes suivantes :

  1. transformez les vecteurs tangent et normal du repère Model au repère View puis normalisez les pour obtenir les vecteurs $t_v$ et $n_v$ respectivement,
  2. calculez le vecteur binormale $b_v$ par produit vectoriel (fonction cross) des vecteurs $t_v$ et $n_v$ (Attention, le produit vectoriel n'est pas commutatif !),
  3. créez la matrice de transformation 3x3 tbnvMatrix dont chaque ligne est constituée des 3 composantes des vecteurs $t_v$, $b_v$ et $n_v$. Si v_1 = vec2(a,b) et v_2 = vec2(c,d) alors m = mat2(v_1,v_2) représente la matrice 2x2 dont la première ligne sera (a,c) et la deuxième (b,d)
  4. les vecteurs $l_t$ et $v_t$ sont obtenus en multipliant la matrice tbnvMatrix par $l_v$ et $v_v$ respectivement.

Avant d'utiliser les normales de la normal map, il faut transformer la valeur lue dans la texture de l'intervalle $[0;1]^3$ vers l'intervalle $[-1; 1]^3$. Utilisez le vecteur obtenu ainsi que $l_t$ et $v_t$ pour calculer les coefficients diffus et spéculaires dans le fragment shader. Pour bien visualiser la perturbation de l'éclairage, affichez d'abord le calcul de l'illumination en niveau de gris sans la texture de couleur.

bump bump bump
rendu original perturbation de l'éclairage résultat final

4 - Motifs procéduraux (facultatif)

1. En utilisant la position des sommets du maillage dans le repère du modèle, écrire un shader permettant d'obtenir une grille procédurale à la surface de l'objet.

L'idée est d'associer une couleur (ici, blanche ou noire) à chaque point de la surface de l'objet. Pour une grille, il suffit de répéter la procédure pour créer des hachures selon les deux directions et de mélanger le résultat. Pour générer des hachures, il faut donc définir une fonction $F$ qui à la position $t$ associe une couleur $c$. Cette fonction à la forme :

fonction
grille
Grille procédurale sans et avec transitions douces

Dans un premier temps, elle peut être obtenue en utilisant la fonction GLSL step(x,t) qui renvoie 0 si $t < x$ et 1 si $t \geq x$. De cette façon, on obtient une seule marche. Pour obtenir plusieurs bandes, il faut moduler la fréquence en utilisant la fonction fract.

2. Les lignes obtenues sont cependant très crénelées. Pour en obtenir des plus douces, vous pouvez utiliser un cosinus au lieu de fract et la fonction mix pour mélanger les couleurs.