TD6 - Textures

Pour démarrer ce TD dans de bonnes conditions, repartez de cette correction partielle des TD précédents : mds3d_td6.zip Vous pouvez désormais contrôler la caméra à l'aide de la souris (classe Trackball) et faire tourner le maillage en pressant la touche 'a'. L'éclairage diffus est calculé dans le fragment shader.

1. Introduction

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 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 (samler) on veut attacher la texture à l'aide de glActiveTexture en la liant avec l'identifiant de texture correspondant en appelant 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. Par exemple, pour indiquer que la variable GLSL uniform sampler2D tex2D doit accéder à l'unité de texture i, on appellera glUniform1i(_shader.getUniformLocation("tex2D"), i).

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. Les maillages earth.obj, cow.obj et plane.obj fournis possèdent déjà des coordonnées de texture, il ne vous reste plus qu'à mettre à jour la fonction Mesh::draw pour les transmettre aux shaders (comme vu aux TD4 et 5 pour les attributs de couleurs et de normales). Pour vérifier que c'est bien le cas, passez les coordonnées de texture du vertex au fragment shader et affichez-les comme une couleur.

texcoord
Coordonnées de texture

2. Placage de texture

1. Dans Viewer::init, chargez "earth.jpg" (depuis le répertoire "data/textures") à l'aide de SOIL_load_OGL_texture (cf. exemple d'utilisation). Créez un shader affichant cette texture à la surface de la sphère. Pour cela, utilisez les coordonnées de texture interpolées dans le fragment shader pour accéder à un sampler2D (déclaré comme variable uniforme) avec la fonction GLSL texture.

terre   nuages  nuit  composite 
Globe terrestre de jour, avec ses nuages, de nuit et mélange entre les trois selon l'éclairage

2. Pour simuler l'éclairage par le soleil, vous allez ajouter la contribution d'une source lumineuse directionnelle, de direction $\vx{l}$ (variable uniforme lightDir). Comme précédemment, utilisez le terme en cosinus : $\cos(\theta) = \max(\vx{n} \cdot \vx{l}, 0)$ où $\vx{n}$ est la normale au fragment, pour faire varier la couleur finale entre celle de la texture et du noir.

3. Pour donner l'impression d'une atmosphère, chargez la texture "earth_clouds.jpg"dans une deuxième unité de texture. Ajoutez un second sampler2D au fragment shader et mélangez la couleur lue dans cette texture avec celle de la terre. Utiliser la fonction GLSL mix pour obtenir un fondu continu.

4. Chargez enfin la texture "earth_night.jpg" – correspondant à la terre vue de nuit – dans une troisième unité de texture. Modifiez le fragment shader pour que les zones non-éclairées utilisent cette texture. Pour cela, effectuez à nouveau un mix avec $\cos(\theta)$ comme poids de mélange.

3. 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). Lors de l'affichage, activez ce sampler et redéfinissez systématiquement ses paramètres de filtrage à _minFilter et _magFilter avec la fonction glSamplerParameteri (vous devrez forcer le cast à GLint). Ainsi, si ceux-ci sont modifiés, ils seront automatiquement mis à jour. Les paramètres du sampler sont utilisés à la place de ceux de la texture à laquelle il est lié (avec glBindSampler). Faites en sorte de filtrer toutes vos textures 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 minification 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 sphère et le maillage "cow.obj" et la texture "cow.jpg" associée. Vous pouvez aussi tester avec un plan ("plane.obj") et la texture "checkerboard.png".

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

int texW = 256;
int texH = 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[texW*texH*3];
glGenTextures(1,&_texID1);
glBindTexture(GL_TEXTURE_2D, _texID1);
for(int l=0; ; ++l) {
    for(int i=0; i<texW*texH; ++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, texW, texH, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    if(texW==1 && texH==1)
      break;
    texW = std::max(1, texW/2);
    texH = std::max(1, texH/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 mip-mapping ?

Important : les niveaux de mip-map é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