gKit2 light
|
cf tuto5GL.cpp
pour modifier l'apparence d'un objet, il est relativement simple de plaquer une image à la surface des triangles. l'objectif est de modifier la couleur des pixels qui sont utilisés pour dessiner l'objet. et l'idée est d'associer un pixel de la texture à chaque pixel utilisé pour dessiner les triangles de l'objet.
il y a plusieurs choses à faire :
c'est le fragment shader qui donne sa couleur à un pixel de l'image. si l'on souhaite utiliser la couleur d'un pixel de la texture, il faut que le fragment shader puisse lire cette couleur. 2 informations sont nécessaires : la texture elle même et les coordonnées du pixel dans la texture.
comment connaitre les coordonnées du pixel dans la texture ? lorsque l'on dessine un triangle, les coordonnées de ses sommets (connues dans le repère de création de l'objet) sont transformées pour déterminer sur quels pixels de l'image résultat se projettent les 3 sommets. puis le pipeline interpole ces coordonnées pixel par pixel pour calculer la profondeur de chaque fragment du triangle (cf. introduction api 3d, openGL et pipeline graphique / partie plusieurs triangles). les coordonnées des sommets dans la texture sont eux aussi interpolés, en même temps, et le fragment shader connait les coordonnées du pixel de la texture correspondant au pixel de l'image. il ne reste plus qu'à lire la couleur dans la texture et à l'utiliser pour colorier le pixel de l'image.
mais il reste un détail à régler : comme précisé dans introduction api 3d, openGL et pipeline graphique, les informations des sommets, les attributs, ne sont pas interpolés automatiquement, il le dire à openGL en déclarant explicitement un varying, une sortie optionnelle du vertex shader et en déclarant l'entrée correspondante dans le fragment shader. plus de détails dans écrire les shaders.
autre explication : le fragment shader ne peut pas accéder aux attributs de sommets, il ne sont connus que du vertex shader, il faut donc créer un varying en sortie du vertex shader pour que le fragment shader récupère cette information (associée aux sommets). et les varyings sont interpolés par le pipeline.
on commence par le cas simple, utiliser une seule texture. première étape, charger une image et créer un objet openGL texture qui stocke l'image sous une forme utilisable par les shaders.
pour créer un objet texture, il faut utiliser glGenTextures( ), la démarche est la même que pour les autres objets openGL :
maintenant que l'objet est créé, il faut le configurer : quelle type de texture, quelles données, etc, cf la famille de fonctions glTexImage(). mais avant, il faut sélectioner l'objet, cf glBindTexture( ) en fonction du type de texture. les textures les plus courantes sont les textures 2d, GL_TEXTURE_2D, qui correspondent à des images classiques :
il ne reste plus qu'à définir les dimensions de la texture, largeur et hauteur, ainsi que le type de ses pixels, combien de canaux couleurs et représentés par des float ou des entiers. le cas classique qui correspond aux images stockées aux formats jpg, png, tga, bmp, etc. est 3 ou 4 canaux couleurs stockés sur des octets (non signés) :
en résumé :
attention : glTexParameter( )
permet de régler plusieurs paramètres modifiant l'accès à la texture. l'exemple ne fournit que les données du level 0. par défaut, openGL utilise plusieurs versions de l'image pour la filtrer : mipmap levels / couches de mipmaps, cf section filtrage et mipmaps. si la ligne glTexParameter(.... GL_TEXTURE_MAX_LEVEL ...)
n'est pas présente, vous aurez une texture noire.
remarque : il y plusieurs solutions pour régler ce problème, le plus simple est de continuer à lire le tuto...
en pratique, on charge l'image en utilisant une librairie et selon le type des pixels de l'image, il faut configurer correctement le format des données transmises à openGL. gKit utilise SDL2_image pour charger les images et renvoie une structure simplifiée qui contient : largeur, hauteur, nombre de canaux couleurs et les données, cf la classe ImageData dans image_io.h.
du coup, un code pour charger une texture avec gKit ressemble à ça :
et comme c'est toujours la même chose, make_texture( )
et read_texture( )
de texture.h font exactement ça.
deuxième étape : fournir les coordonnées dans la texture des sommets de l'objet, il faut les stocker dans un vertex buffer et décrire correctement le format de sommet (cf configurer un format de sommet, vertex array object).
le cas simple est de créer un nouveau buffer pour stocker les coordonnées :
et d'ajouter cet attribut au format de sommet :
remarque : les coordonnées dans la texture sont en 2d, comme on peut s'y attendre, cf 2, GL_FLOAT pour openGL et vec2 pour GLSL (les shaders) pour les représenter.
attention : l'api openGL et le langage des shaders, GLSL, n'appellent pas les types de la même manière, un vec2 GLSL est décrit comme 2, GL_FLOAT dans l'application pour l'api openGL. un vec3 GLSL, est décrit par 3, GL_FLOAT, etc.
troisième étape, les shaders !
première chose à faire, modifier le vertex shader pour qu'il récupère les coordonnées de texture et les transmette au fragment shader.
le vertex shader déclare un attribut, texcoord par exemple et un varying, une sortie optionnelle, vertex_texcoord : (repassez dans introduction api 3d, openGL et pipeline graphique et shaders et GLSL, si nécessaire)
de son coté, le fragment shader récupère les coordonnées avec le varying vertex_texcoord :
il ne reste plus qu'à lire la couleur de la texture aux coordonnées stockées dans vertex_texcoord... c'est la famille de fonctions texture( ) de GLSL qui permet de le faire. le premier paramètre de texture( ) est un paramètre uniform dont le type dépend du type de la texture : pour une texture 2D, c'est sampler2D. texture( ) renvoie un vec4 qui correspond aux 4 canaux de la couleur : rgb et a.
remarque : même si la texture ne contient pas 4 canaux pour décrire une couleur, la fonction texture() renvoie toujours un vec4, les composantes non utilisées ont une valeur par défaut (0 pour rgb et 1 pour a). exemple : une texture GL_RG, qui ne contient que 2 canaux, renverra vec4(r, g, 0, 1);
remarque : texture( )
suppose que les coordonnées de texture sont normalisées entre 0 et 1, ce qui permet d'oublier les dimensions de l'image... les coordonnées stockées dans les fichiers .obj
sont normalisées, en général. mais il existe d'autres fonctions, comme texelFetch()
qui utilisent directement les dimensions de l'image.
au final, les 2 shaders ressemblent à ça :
il ne reste plus qu'à configurer le pipeline pour dessiner un objet texturé !
quatrième étape, configurer le pipeline :
qu'est qui a changé ?
glBindVertexArray(vao);
glUseProgram(program);
glUniform1i(location, value);
glBindTexture(GL_TEXTURE_2D, texture);
pour sélectionner la texture, c'est glBindTexture(), comme lors de sa création. l'uniform est un peu particulier, pour le shader c'est un sampler2D
et pour l'application c'est un int
qui représente l'indice de l'unité de texture. par défaut, lorsque l'application n'utilise qu'une texture, c'est 0.
au final : configurer le pipeline pour utiliser une texture dans le fragment shader ressemble à ça :
le code complet ressemble a tuto5GL.cpp
comme suggéré dans la section création des textures, une texture est composée de plusieurs niveaux / couches... pourquoi ?
lorsqu'on observe un triangle "à la bonne distance", un pixel de l'image correspond à un pixel de la texture. dans ce cas, l'affichage est propre.
par contre, si on s'éloigne, plusieurs pixels de la texture se projettent sur un pixel de l'image. quelle couleur donner au pixel de l'image ? la bonne réponse est : la moyenne des couleurs des pixels de la texture qui se projettent dans le même pixel de l'image. mais cette solution pose un gros problème : plus l'objet est loin, plus il faut de pixels de la texture pour calculer la moyenne, et plus le calcul est long. pour éviter ce ralentissement en fonction de la distance, on peut précalculer les moyennes à des résolutions différentes : pour des blocs de 2x2 pixels, de 4x4, de 8x8, de 16x16, etc. et choisir la bonne taille de bloc qui contient la moyenne précalculée. openGL appelle ces versions pré-filtrées ou pré-calculées de la texture des mipmaps.
pour décrire complètement une texture à openGL, il faut donner toutes ces versions pré-calculées de l'image, donc plusieurs levels pour une résolution de base. pour une image 1024x1024, il faudra les versions 512x512, 256x256, 128x128, etc. jusqu'a 1x1. c'est un peu long à écrire, mais pas difficile :
remarque : pour une image rectangulaire, par exemple 1024x512, les versions sont 512x256, 256x128, etc. 2x1 et 1x1
remarque : le niveau 0 est l'image complète, le niveau 1 l'image pré filtrée dont la résolution est divisée par 2 sur chaque axe, etc. en résumé, le niveau level à des dimensions : \( \mbox{width}= \mbox{image.width} / 2^{level} \), et \( \mbox{height}= \mbox{image.height} / 2^{level} \). ce qui s'écrit directement :
remarque : on peut écrire la fonction miplevels() simplement avec une boucle :
exercice : ecrivez la fonction make_miplevel( const ImageData& image, const int level )
.
mais il y a encore plus simple, il suffit de demander à openGL de faire les pré-calculs ! cf glGenerateMipmap( ) :
comme ces niveaux de mipmaps ne sont qu'une approximation du vrai calcul, il y a des manières différentes de les utiliser et il faut donc configurer le pipeline... et comme il y a pas mal d'options, elles sont regroupées dans un objet openGL, un sampler, qu'il faut créer, cf glGenSamplers() et configurer, cf glSamplerParameter( ).
voici les paramètres par défaut qu'utilise openGL ;
il faut aussi configurer le pipeline lors du glDraw( ), cf glBindSampler( ). lorsqu'il n'y a qu'une seule texture :
au final, le code complet ressemble à tuto5GL_sampler.cpp
lorsqu'un fragment shader veut utiliser plusieurs textures en même temps pour calculer la couleur d'un pixel, openGL utilise le même principe que pour les attributs de sommets : elles sont numérotées. les textures et leurs paramètres de filtrage (sampler) sont décrits dans plusieurs unités de textures. le fragment shader est configuré (l'uniform sampler2D déclaré par le shader) avec l'indice de l'unité sur laquelle est sélectionnée la texture et ses paramètres de filtrage.
openGL définit entre 16 et 32 unités de texture, il suffit de les sélectionner avec glActiveTexture( ) et de les configurer 'normalement' avec glBindTexture( ), et glBindSampler( ).
attention : la numérotation des unités de texture n'est pas très cohérente, c'est GL_TEXTURE0
, GL_TEXTURE1
, etc. pour glActiveTexture( )
et 0
, 1
pour glBindSampler( )
exemple :
et dans l'application, il faut bien sur créer les 2 textures, créer au moins un sampler, puis configurer le pipeline pour le draw.
code complet dans tuto5GL_multi.cpp