Introduction | ⇑ |
Autant vous le dire tout de suite : le texturing est un sujet tellement vaste
qu'il ne suffirait pas d'un seul tutorial pour en faire le tour, ni même
3 ou 4. Les possibilités qu'offrent OpenGL sont tellement vastes dans
ce domaine que les applications de cet outils sont quasi infinies. Donc là,
on va juste voir les bases (ce qui vous permettra quand même de faire
des trucs bien sympa), c'est-à-dire comment déclarer une texture
à OpenGL, l'appliquer sur un polygone grâce aux TexCoord, et aussi
quelques outils qui vont avec tel que le filtering, la répétition...
Ca en fait déjà pas mal je pense, et pour ce qui est du mipmapping,
de la transparence ou des textures d'environnement, on verra ça plus
tard, OK ? | |
1. C'est parti | ⇑ |
Commencons par le commencement, c'est-à-dire par activer la gestion
des textures. Si vous avez bien tout pigé, vous aurez deviné qu'il
suffit de faire un petit glEnable()
avec le paramètre approprié. Vous avez gagné ! Rajoutez
donc dans notre petite InitGL() la
ligne suivante :
glEnable(GL_TEXTURE_2D);
Ca active la gestion des textures 2D (ben ouais, ca existe les textures 1D
ou 3D). Nous devons maintenant créer un nouvel objet texture. Pour nous,
ce sera juste un numéro, mais c'est ce numéro qui nous permettra
de spécifier à OpenGL toutes les paramètre de la texture,
la texture elle-même, et aussi de lui dire quelle texture on veut mapper.
Pour cela il faut utiliser glGenTextures(), qui
nous renvoie autant de numéros que de textures demandées. Ici
on veut qu'une seule texture, donc on va récupérer un numéro,
à bien retenir.
Ensuite, nous décidons quelle est la texture courante grâce à
glBindTexture() . La texture courante,
c'est celle sur laquelle OpenGL va effectuer les modifs lorsqu'on lui demandera,
et c'est aussi la texture qiu sera mappée sur les objets. Donc si vous
avez des objets avec des textures différentes, il suffira d'appeler glBindTexture()
avec le bon numéro de texture juste avant de dessiner l'objet sur lequel
devra être mappé la texture.
Et enfin, nous allons transmettre à OpenGL toutes les caractéristiques
de la texture : largeur, hauteur, format, etc... et bien sûr l'image elle-même,
grâce à glTexImage2D() .
Pour l'instant, pour simplifier les choses, j'ai choisi d'appliquer à
notre objet une texture de 2 pixels par 2 pixels, en forme d'échiquier
(et ouais, je me suis pas foulé). Voilà donc tout ce que nous
devons écrire :
GLubyte Texture[16] =
{
0,0,0,0, 0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF, 0,0,0,0
};
|
//Image (2x2) |
GLuint Nom; |
|
void InitGL() |
{ |
|
|
glClearColor(.5,.5,.5,0); |
//Change la couleur du fond |
glEnable(GL_DEPTH_TEST); |
//Active le depth test |
glEnable(GL_TEXTURE_2D); |
//Active le texturing |
glGenTextures(1,&Nom); |
//Génère un n° de texture |
glBindTexture(GL_TEXTURE_2D,Nom); |
//Sélectionne ce n° |
glTexImage2D ( |
|
GL_TEXTURE_2D, |
//Type : texture 2D |
0, |
//Mipmap : aucun |
4, |
//Couleurs : 4 |
2, |
//Largeur : 2 |
2, |
//Hauteur : 2 |
0, |
//Largeur du bord : 0 |
GL_RGBA, |
//Format : RGBA |
GL_UNSIGNED_BYTE, |
//Type des couleurs |
Texture |
//Addresse de l'image |
); |
|
glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
glTexParameteri(GL_TEXTURE_2D,
GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
} |
|
Voilà, j'ai fait ce que j'avais annoncé : J'appelle glGenTextures()
avec le nombre de textures voulues et le tableau dans lequel il doit les renvoyer
(ici il n'y en a qu'une donc autant mettre &Nom ).
J'appelle ensuite glBindTexture()
pour lui dire avec quelle texture je vais travailler. Et enfin, glTexImage2D() ,
pour lui balancer toutes les infos dont il a besoin. Les commentaire vous disent
déjà à quoi correspondent les paramètres, mais je
vais quand même insister sur 2 ou 3 choses :
- Pour ce qui est du mipmap, j'ai dit qu'on verra ca plus tard, donc je n'en
parlerais pas. Contentez-vous de mettre ce paramètre à 0
- Le 3e paramètre c'est le nombre de composantes de couleurs par pixel.
Ici il y en a 4 : R,G, B et A (pour Rouge, Vert, Bleu et Alpha, pour ceux
qu'auraient pas encore tout pigé). On peux en avoir de 1 à 4.
- Ensuite viennent la largeur et la hauteur. Elles ne sont pas forcément
égales, mais doivent être impérativement de la forme
2^n. Si une de ces valeurs n'est pas une puissance de 2, ca ne marchera
pas. Si vous êtes obligé de bosser sur des textures batardes,
utilisez
gluScaleImage() pour les
mettre à l'échelle (voyez avec votre compilateur ou le Reference
Manual pour en savoir plus sur cette fonction).
- Le paramètre suivant est la largeur du bord de la texture. En effet,
on peut rajouter une bordure d'un pixel autour de l'image, et d'une couleur
unique. Cela peut servir pour éviter d'avoir des résultat bizarre
sur les bords avec le filtering, mais on ne l'utilise pas souvent.
- Vient ensuite le format de stockage. Ici, j'ai rangé les octets de
ma texture dans le format classique RGBA, donc je dis à OpenGL de les
lire suivant le format RGBA. On peut mettre
GL_COLOR_INDEX,
GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_RGB, GL_RGBA, GL_BGR_EXT, GL_BGRA_EXT,
GL_LUMINANCE, et GL_LUMINANCE_ALPHA .
Amusez-vous à changer ce paramètre pour voir ce que ca donne.
- Le 8e paramètre définit sous quel type sont enregistrées
les composantes. Ici, ma texture est un tableau de
GLubyte (c'est-à-dire
unsigned char), donc je dit à OpenGL de les lire en tant que GLubyte .
Ce paramètre peut être GL_UNSIGNED_BYTE,
GL_BYTE, GL_BITMAP, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT,
ou GL_FLOAT .
- Et enfin, on donne à OpenGL l'endroit où est stockée
l'image, histoire qu'il puisse la mapper.
Remarque : Vous vous êtes peut-être
demandé pourquoi j'utilise une texture RGBA et pas RGB, alors qu'on a
même pas envore vu ce qu'était la composante A. En fait, je fais
ça pour deux raisons : d'abord, comme on bosse sur des processeurs 32
bits, la gestion de la mémoire est optimisée pour les types 32
bits. De plus, si vous essayez de mettre une texture RGB à la place,
il y de fortes chances pour que le résultat ne soit pas celui que vous
attendez. En effet, par défaut, OpenGL considère que chaque pixel
occupe 4 octets. Donc il va lire les pixels de l'image 4 par 4, ce qui n'est
pas vraiment ce qu'on veut. Pour résoudre ce problème, il faut
appeler glPixelStorei(GL_UNPACK_ALIGNMENT,1) ,
mais je n'en dirais pas plus, car si on veut quelque chose d'optimisé
et de compatible, on utilise des textures RGBA.
Je vous entends déjà dire "Et les deux dernières
lignes ?". Pour l'instant je n'en parle pas, contentez-vous de les recopier
bêtement, on verra ça plus tard. | |
2. Coordonnées de textures (TexCoord) | ⇑ |
Et ben ça y est, mine de rien, notre texture est chargée et prète
à être apliquée : on n'a plus qu'à dessiner un truc
sur lequel l'appliquer. Pourquoi pas un joli p'tit cube ? Et ben allons-y alors
!
void Draw() |
{ |
|
|
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
); |
//Efface le framebuffer et le depthbuffer |
glMatrixMode(GL_MODELVIEW); |
//Un petit gluLookAt()... |
glLoadIdentity(); |
gluLookAt(3,2,3,0,0,0,0,1,0); |
glBegin(GL_QUADS); |
//Et c'est parti pour le cube ! |
glTexCoord2i(0,0);glVertex3i(-1,-1,-1);
glTexCoord2i(1,0);glVertex3i(+1,-1,-1);
glTexCoord2i(1,1);glVertex3i(+1,+1,-1);
glTexCoord2i(0,1);glVertex3i(-1,+1,-1);
|
//1 face |
glTexCoord2i(0,0);glVertex3i(-1,-1,+1);
glTexCoord2i(1,0);glVertex3i(+1,-1,+1);
glTexCoord2i(1,1);glVertex3i(+1,+1,+1);
glTexCoord2i(0,1);glVertex3i(-1,+1,+1);
|
//2 faces |
glTexCoord2i(0,0);glVertex3i(+1,-1,-1);
glTexCoord2i(1,0);glVertex3i(+1,-1,+1);
glTexCoord2i(1,1);glVertex3i(+1,+1,+1);
glTexCoord2i(0,1);glVertex3i(+1,+1,-1);
|
//3 faces |
glTexCoord2i(0,0);glVertex3i(-1,-1,-1);
glTexCoord2i(1,0);glVertex3i(-1,-1,+1);
glTexCoord2i(1,1);glVertex3i(-1,+1,+1);
glTexCoord2i(0,1);glVertex3i(-1,+1,-1);
|
//4 faces |
glTexCoord2i(1,0);glVertex3i(-1,+1,-1);
glTexCoord2i(1,1);glVertex3i(+1,+1,-1);
glTexCoord2i(0,1);glVertex3i(+1,+1,+1);
glTexCoord2i(0,0);glVertex3i(-1,+1,+1);
|
//5 faces |
glTexCoord2i(1,0);glVertex3i(-1,-1,+1);
glTexCoord2i(1,1);glVertex3i(+1,-1,+1);
glTexCoord2i(0,1);glVertex3i(+1,-1,-1);
glTexCoord2i(0,0);glVertex3i(-1,-1,-1);
|
//6 faces |
glEnd(); |
|
SwapBuffers(DC); |
//glutSwapBuffers(); pour GLUT |
glutPostRedisplay(); |
//Uniquement pour GLUT |
} |
|
Et voilà. Si vous lancez le programme maintenant, ca va donner une
magnifique image de cube mappé avec une texture d'échiquier, comme
ça :
Euh, bah
non en fait elle est pas si magnifique que ca l'image : y s'passe des trucs
bizarres. En fait, c'est parce que OpenGL y va un peu trop à la barbare
pour appliquer les textures. En effet, si on réfléchit bien,
on se rend compte que pour que l'effet de mapping soit réaliste,
il faut tenir compte de la profondeur. Prenons l'exemple d'un triangle dont
un des sommets est très éloigné de l'utilisateur par
rapport aux deux autres, comme ça :
Là la texture est correctement appliquée, et on a bien l'impression
que le triangle est aligné sur l'axe Z. |
|
Et bien si on ne tient pas compte de la profondeur, la texture est appliquée
comme si le triangle était face à nous, comme OpenGL le
fait actuellement pour notre cube, et ca donnerait ça :
Et là, c'est carrément pas ce qu'on veut. Donc il faut dire
à OpenGL d'effectuer une correction de perspective sur les textures
(et par la même occasion sur les couleurs). Pour cela, rajoutez
dans InitGL() la ligne suivante
:
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);
|
|
Et maintenant,
voilà la magnifique image qu'on obtient. C'est quand même un petit
peu mieux, non ?
Bon maintenant voyons un peu ce que notre fonction a dans la bide : ici on découvre
une nouvelle fonction : glTexCoord{1234}{sifd}[v]() .
C'est elle qui nous permet de dire à OpenGL quel morceau de la texture
est attaché à quel sommet. Ici on a une texture 2 dimensions,
donc on utilise glTexCoord2*() . On
lui passe en paramètre 2 valeurs, qui sont les coordonnées du
point de la texture à attacher au sommet qui va suivre. Le point (0,0)
correspond au coin supérieur gauche de la texture, et (1,1) au coin inférieur
droit. Mais on peut aussi choisir n'importe quel point de la texture (ou texel)
entre 0 et 1. Par exemple si on avait voulu mettre que du blanc, il aurait suffit
de mapper la partie supérieure gauche de la texture, donc de remplacer
les 1 par des 0.5 (et aussi d'utiliser glTexCoord2f() ,
parce que sinon les TexCoord auraient été transformées
en int). |
| |
3. Filtering | ⇑ |
Imaginez que vous êtes dans un simulateur de vol dernier cri : si vous
regardez le sol, et même si vous êtes très près, normalement
vous ne disinguez pas les pixels qui composent la texture. Alors que si vous
jouez à un vieux je du genre de Duke Nukem ou Quake, si vous vous collez
à un mur vous verrez distinctement les pixels de la texture, comme dans
notre exemple. Mais dans Quake 3, ou dans un simulateur de vol, vous ne verrez
jamais la séparation nette entre les pixels : il y aura toujours un dégradé
entre le pixel et son voisin. Cette petite révolution du monde de la
3D s'appelle le bilinear filtering, et est parfaitement gérée
par OpenGL. Vous vous rappelez les deux lignes que je n'ai pas expliqué
tout-à-l'heure ? Et bien elles disaient à OpenGL de ne pas faire
de bilinear filtering, mais de prendre le texel le plus proche (d'où
le terme GL_NEAREST ) lorsqu'il dessine
le pixel d'un polygone. Changez donc ce paramètre en GL_LINEAR
dans les deux lignes, et voyez le résultat à droite.
On voit bien
qu'il y a un dégradé entre chaque texel, d'ailleurs on le voit
un peu trop bien, puisque là on a une texture de 4 pixels (2x2), ça
fait un peu juste, mais bon. Voyons plutôt cette fonction
glTexParameter{if}[v]() : avec elle on règle le filtering,
mais en 2 fois. Pourquoi ? Et bien en fait il y a 2 filtering : un lorsqu'il
y a magnification (GL_MAG ), c'est-à-dire
lorsque un texel s'étend sur plusieurs pixels, comme ici, en un lorsqu'il
y a minification (GL_MIN ), c'est-à-dire
lorsqu'un pixel contient plusieurs texels. Dans le cas de la magnification,
on l'a vu, le pixel est le résultat de la moyenne des texels environnants.
Dans le cas de la minification, s'il y a filtering, le pixel est le résultat
de la moyenne des texels contenus. |
| |
4. Répétition | ⇑ |
Mais il y a quand même un truc bizarre : sur les bords des faces, la
texture subi un dégradé vers le gris, alors que normalement elle
devrait subi un dégradé vers la coumeur du pixel de la texture
(blanc ou noir). Pourquoi ? Et bien parce qu'OpenGL est configuré pour
répéter la texture. C'est-à-dire que si on avait mis une
valeur n>1 pour les texcoords, il aurait répété n fois
la texture (essayez pour voir). Et lorsqu'il est au bord, il fait un filtering
avec le bord opposé de la texture, ce qui conduit à rendre tous
les bords... gris !
En mode GL_CLAMP ("bridé"),
les coordonnées sont ramenées entre 0 et 1 et sur les côtés
de la texture, le filtering est fait entre la texture et le bord (celui que
l'on spécifie dans glTexImage2D()
). Pour choisir le mode de texuring ,
il faut appeler glTexParameter() avec
comme premier paramètre GL_TEXTURE_2D ,
comme second paramètre GL_TEXTURE_WRAP_S
(pour affecter les coordonnées horizontales) ou GL_TEXTURE_WRAP_T
(pour les coord. verticales), et comme troisième paramètre GL_CLAMP
(pour bridé) ou GL_REPEAT (pour
répété). Et si on est en mode répété,
on obtient bien le résultat que voici. Ici, comme la couleur du bord
est (0,0,0,0) par défaut, il y a un dégradé vers le noir
sur les bords des faces.
Vous êtes en train de vous demander comment ne pas faire de filtering
sur les bord ? Et ben vous pourrez chercher longtemps, parce que... on peut
pas. Hé non ! Donc le meilleur moyen est encore de se mettre en GL_REPEAT
de de faire des textures dont les bords opposés sont semblables, ou d'utiliser
le bord en mode GL_CLAMP si les couleurs
ne varient pas trop.
[Rectification : Maintenant, avec l'extension ARB_texture_border_clamp,
il est possible de ne pas faire de filtering sur les bords. Il
suffit d'utiliser la constante CLAMP_TO_BORDER_ARB
(ou CLAMP_TO_BORDER_ARB , suivant votre version d'OpenGL)
à la place de GL_REPEAT ou GL_CLAMP .]
Ah oui j'oubliais : pour changer la couleur du bord, on
appelle glTexParameter{if}v() avec
GL_TEXTURE_BORDER_COLOR comme 2e paramètre et un tableau de 4 valeurs
(RGBA pour changer) comme 3e paramètre. |
| |
Conclusion | ⇑ |
On en a déjà vu pas mal dans ce tutorial, et pourtant il reste
encore beaucoup à dire sur le sujet : il y a le mipmapping, le blending...
Et saviez-vous par exemple que les textures possèdent aussi leur propre
matrice ? Ben ouais, après tout, appliquer une texture, c'est une simple
affaire de projection, non ? (ouh là, je commence à me Weberiser,
moi - private joke). Donc tout ça, on le verra plus tard, parce que moi,
là, j'ai faim. Alors amusez-vous bien avec les sources que vous pouvez
télécharger en bas, et avec l'annexe que voici. | |
Annexe : charger des images BMP | ⇑ |
Bon OK je dois bien l'avouer, y'a pas que des échiquers de 4
pixels dans le monde de la 3D. Généralement, y'a plutôt
des textures assez complexe qu'on aurait du mal à la main, mais qui,
bizarrement, sont plus facilement éditables sous formes d'images (.bmp
par exemple...). Hé hé, je vous vois venir : "cool, il va
nous filer un code tout prêt pour
loader des bmp !!". Et ben nan, na ! D'abord je me suis fais chier à
décrire le format BMP sur ce site (allez, cherchez un peu), alors c'est
pas pour des prunes, vous pouvez faire un loader vous-mêmes, ca devrait
durer 15 min à tout casser. Mais bon pour les quiches qui auront le courage
de pomper le tutorial et les sources, je vais quand même inclure une petite
fonction de chargement de BMP, histoire de pas être trop salaud (vous
pouvez voir le résultat à droite).
Et si vous n'avez vraiment pas envie de vous cassez le cul, il existe dans la
librairie glaux.h une fonction toute faite pour ce genre de choses. Allez sur
le site de Nehe pour voir comment l'utiliser.
Sinon, pour ceux qui auraient l'intention de charger leurs images tous seuls,
quelques remarques :
- D'abord, n'oubliez pas que l'image est inversée. Cela veut non seulement
dire que la ligne du bas est stockée en premier, mais aussi que le
format est donc BGR, et non RGB. Donc logiquement il suffirait d'appeler glTexImage2d()
avec comme format GL_BGR_EXT et paf ca marche. En effet, théoriquement,
ça marche. Mais pas partout. En effet, sur certaines cartes 3D (des
ATI All-InWonder, pour ne pas le nommer), la texture était quand même
chargée en RGB. Donc préférez inerser les composantes
R et B de chaque pixel avant d'envoyer la texture à OpenGL, ca vous
évitera de faire des patchs. Vous pouvez aussi par la même occasion
la mettre sens dessus-dessous (je veux dire faire un 'flip' pour la remettre
dans le bon sens).
- Ensuite, gare au champ 'Taille de l'image' à l'offset 0x22 dans les
fichiers BMP : il n'est pas toujours rempli par tous les logiciels, car on
peu le retrouver grâce à la largeur et la hauteur. Donc éviter
de l'utiliser.
Antoche | |