Programmation Graphique : CM séance 04
7. Langage GLSL
Le langage GLSL est le principal langage de shaders d'OpenGL (il existe par exemple des langages spécifiques à NVidia et ATI).
Le langage s'inspire de C et de C++, avec du calcul vectoriel et matriciel intégré.
Les numéros de version suivent celles d'OpenGL depuis la version 3.3.
Pour une description complète de GLSL voir le wiki Khronos.
7.1. Les types de données
Le type de retour d'une fonction qui ne renvoie pas de valeur est void
.
Il n'y a ni pointeur ni référence.
De nombreux types sont disponibles pour faciliter les calculs des opérations graphiques.
7.1.1. Scalaires et littéraux
Les types scalaires :
bool
un booléen valanttrue
oufalse
;int
un entier 32 bits signé ;uint
un entier 32 bits non signé ;float
un flottant simple précision (32 bits) ;double
un flottant double précision (64 bits, version 4.0+).
Les littéraux suivent les règles suivantes :
true
etfalse
sont desbool
;- toute valeur entière est un
int
par exemple123
; - pour déclarer un
uint
il faut le suffixer paru
ouU
, par exemple123u
; - les valeurs entières peuvent aussi être exprimées en base 8 ou 16, en
les préfixant par
0
ou0x
; - une valeur avec un point est par défaut un
float
, par exemple0.
,.0
ou0.0
; pour obtenir unfloat
à partir d'un littéral entier il faut le suffixer parf
ouF
, par exemple0f
; - un
float
peut aussi être exprimé en notation scientifique avece
ouE
, par exemple1e+3
qui vaut100f
; - pour obtenir un
double
on suffixe parlf
ouLF
, par exemple3.14lf
(version 4.0+).
Certaines conversions implicites sont effectuées (dans les premières versions, il pouvait y avoir des erreurs selon le constructeur et le driver).
7.1.2. Les vecteurs
Les vecteurs sont des tableaux de taille 2 à 4, exprimés sous la forme
[diub]vec[234]
:
vec2
,vec3
,vec4
: un vecteur defloat
;dvec2
,dvec3
,dvec4
: un vecteur dedouble
;ivec2
,ivec3
,ivec4
: un vecteur deint
;uvec2
,uvec3
,uvec4
: un vecteur deuint
;bvec2
,bvec3
,bvec4
: un vecteur debool
.
Les vecteurs peuvent être initialisés avec un constructeur, par exemple :
vec2 texcoord (0.0, 0.5);
vec3 couleur (1.0, 0.0, 0.0);
vec4 position (0.5, 0.0, -1.0, 1.0);
Pour accéder à un élément d'un vecteur on peut utiliser des indices, ou des champs dont les noms sont prédéfinis :
[0]
,[1]
,[2]
,[3]
par indice ;.x
,.y
,.z
,.w
pour les positions ;.r
,.g
,.b
,.a
pour les couleurs ;.s
,.t
,.p
,.q
pour les coordonnées de texture.
Par exemple :
texcoord.s = texcoord.t;
couleur.r = couleur.g = couleur.b = 1.0;
position.x = (position.y + position.z) / position.w;
On peut en fait utiliser indifféremment l'une des 4 notations (sans tenir compte de la quantité représentée). Cette notation s'appelle le swizzling (mélange) ; elle permet d'agir sur plusieurs dimensions en même temps avec un swizzle mask :
texcoord.st = texcoord.ts;
couleur = couleur.bgr;
position = vec4(position.yzx, 1.0);
Dans un swizzle mask,
- la dimension est donnée par le nombre de lettres ;
- on ne peut pas mélanger des notations, par exemple on ne peut pas écrire
.xr
.
Le swizzling fonctionne aussi sur les Lvalues :
texcoord.st = vec2(0.5, 0.7);
couleur.bg = vec2(0.0, 1.0);
position.zyx = vec3(0.0, 1.0, 0.5);
Un même champ ne peut pas apparaître plusieurs fois dans une Lvalue :
position.xxz = vec3 (1.0, 1.0, 3.0); // erreur
7.1.3. Les matrices
Le langage dispose également de matrices, de dimension 2, 3 ou 4.
Le type s'écrit sous la forme : [d]mat[234]
pour les matrices carrées :
- les matrices
mat
sont des matrices defloat
; - il y a aussi des matrices
dmat
dedouble
.
Par exemple : mat4
, dmat3
.
Il y a aussi des matrices rectangulaires, dont le type s'écrit :
[d]mat[234]x[234]
(colonnes x
lignes).
Par exemple : mat2x3
est une matrice de float
de 2 colonnes et 3 lignes.
On accède à un élément par les indices [colonne][ligne]
.
Par exemple
mat3 M;
M[2][0] = 1.0; // 3e colonne, 1ère ligne
Cet ordre, appelé column-major order, est l'inverse de l'ordre mathématique habituel lignes*colonnes, et permet de manipuler une matrice comme une liste de vecteurs-colonne :
mat3 M;
M[1] = vec3(1.0, 2.0, 1.5);
M[0].yxz = vec3(0.0, -1.0, 0.5);
7.1.4. Types opaque
Un type opaque représente un objet externe, dont la manipulation est limitée. Une variable de type opaque peut être déclarée comme :
- une globale
uniform
(donnée partagée par tous les shaders) ; - un membre d'un
struct
; - un paramètre d'entrée d'une fonction.
Une variable de type opaque ne peut pas être une Lvalue.
On peut définir un array
de type opaque ; on accède alors aux éléments
par l'indice [i]
et à la longueur avec .length()
.
Les types sampler*
pour désigner des textures, sont des exemples de types opaque
(vus plus loin).
7.1.5. Tableaux
Des éléments de même type peuvent être regroupés dans un tableau (array).
Sauf exceptions, la taille doit être connue à la compilation.
Elle peut être retrouvée avec la méthode length
:
float values[8];
int n = values.length();
Les tableaux de tableaux sont possible à partir de GLSL 4.3+.
Les indices pour accéder à un élément de tableau peuvent être non constants
(par exemple une variable entière dans une boucle), sauf si le tableau
est opaque (i.e. il contient un type opaque, ou un struct
qui contient un type opaque) :
- en GLSL 3.3, on ne peut pas boucler sur un tableau opaque ;
- en GLSL 4.0+, on peut boucler sur un tableau opaque dans le cas où l'indice est dynamiquement uniforme.
7.1.6. Structures
Les structures sont déclarés dans la syntaxe du C++, sans typedef
:
struct MyData {
float x, y;
vec4 couleur;
};
Utilisation :
MyData d;
d.x = 1.0;
d.couleur = vec4(1.0, 0.0, 0.0, 1.0);
Un struct
qui contient un type opaque est considéré comme opaque.
Les struct
peuvent être utilisés partout (variables locales ou globales,
paramètres de fonction), sauf comme variables d'entrée-sortie des shaders.
7.2. Éléments du langage
7.2.1. Préprocesseur
GLSL supporte la plupart des directives de précompilation du C, à l'exception
notable de #include
:
#define NOM définition
,#undef NOM
;#if
,#ifdef NOM
,#ifndef NOM
;#else
,#elif
,#endif
;#error
: provoque une erreur ;#pragma
: indication au compilateur ;#line
: modifie le compteur de ligne ;__LINE__
: ligne courante ;__FILE__
: fichier courant, sous la forme d'un numéro ;__VERSION__
: version actuelle de GLSL ;#version numéro
: version de GLSL demandée (obligatoire pour core profile) ;#extension NOM : require | enable | warn |disable
pour tester la présence ou activer une extension.
7.2.2. Structures de contrôle
GLSL utilise les structures de contrôle habituelles du C :
- les branchements
if
-else
etswitch
-case
; - les boucles
for
,while
etdo
-while
; - les sauts
break
,continue
,return
mais pasgoto
.
On peut déclarer une variable dans un for
mais pas dans un if
.
Le nouveau mot clé discard
est introduit, uniquement pour les fragment shader :
il permet de sortir du shader en supprimant le fragment pour la suite du pipeline.
7.2.3. Fonctions
Les fonctions de GLSL ont les propriétés suivantes :
- la première fonction appelée est
main
, elle ne prend pas d'arguments et ne renvoie rien ; - les fonctions peuvent renvoyer une valeur avec
return
, ou rien avec le type de retourvoid
; - les récursions sont interdites.
Le paramètres et la valeur de retour sont uniquement passés par recopie, ils ne peuvent pas être transmis par référence ou adresse. À la place il y a les qualificatifs suivants :
in
pour les variable en entrée ;const in
pour les variables en entrée, ne pouvant être changée par la fonction ;out
pour les variables en sortie ;inout
pour les variables en entrée et en sortie.
Les variables out
et inout
doivent être des Lvalues.
Exemple :
void change_color (in float f, inout float r, inout float g, inout float b)
{
r *= f; g *= f; b *= f;
}
float r = 1.0, g = 0.5, b = 0.0;
change_color (1.2, r, g, b);
7.2.4. Initialisation et constructeurs
Lors de la déclaration, les variables de type de base peuvent être initialisées par affectation :
float x = 0.5;
int i = 4;
Le cast explicite est possible avec la syntaxe type(valeur)
, par exemple
int(3.2)
.
Les types plus élaboré sont initialisés avec un constructeur, de la forme
nom_du_type(valeur, ...);
par exemple
vec3 v = vec3(1.0, 2.0, 3.0);
Vecteurs
Les constructeurs de vecteur peuvent prendre une seule valeur, par exemple
vec3(t)
est équivalent à vec3(t,t,t)
.
Ils peuvent aussi prendre d'autres vecteurs ; les valeurs sont prises de gauche à droite. La dimension n'a pas besoin de correspondre, il suffit d'avoir suffisamment de valeurs :
vec3(vec2(1.0, 2.0), 3.0); // ok, vec3(1.0, 2.0, 3.0)
vec4(vec3(1.0, 2.0, 3.0)); // erreur, terme manquant
vec3(vec4(1.0, 2.0, 3.0, 4.0)); // ok, vec3(1.0, 2.0, 3.0)
Matrices
-
Si on fournit une seule valeur, elle est copiée sur la diagonale, les autres cases sont mises à 0 ; par exemple
mat4(1.0)
est la matrice identité. -
Si on fournit plusieurs valeurs, elles remplissent la matrice dans l'ordre colonne-ligne ; les cases non fournies sont remplies avec la matrice identité.
-
Les valeurs peuvent aussi êtres passées sous forme de vecteur.
Par exemple :
mat2( 2.0, 3.0, // première colonne
4.0, 5.0 ); // deuxième colonne
fabrique la matrice \(\left(\begin{array}{cc} 2.0 & 4.0 \\ 3.0 & 5.0 \end{array}\right)\) , et
mat3( vec3( 2.0, 3.0, 4.0), // première colonne
vec2( 5.0, 6.0), 7.0, // deuxième colonne
8.0, 9.0, 10.0 ); // troisième colonne
construit la matrice \(\left(\begin{array}{ccc} 2.0 & 5.0 & 8.0 \\ 3.0 & 6.0 & 9.0 \\ 4.0 & 7.0 & 10.0 \end{array}\right)\) .
On peut aussi fabriquer une matrice à partir d'une autre matrice, de dimension inférieur ou supérieur, selon le principe que seule la partie qui correspond est copiée, et le reste est rempli par la matrice identité. Par exemple
mat3( mat4x2( 1.0, 2.0, // colonne 1
3.0, 4.0, // colonne 2
5.0, 6.0, // colonne 3
7.0, 8.0 )); // colonne 4
Tableaux et structures
Les tableaux peuvent être initialisées avec la syntaxe du constructeur, en rajoutant
les []
:
float a[3] = float[3]( 1.0, 2.0, 3.0 );
float a[3] = float[]( 1.0, 2.0, 3.0 ); // variante
Pour les structures, on donne les paramètres dans l'ordre de leur déclaration :
struct Foo {
float bar;
vec3 baz;
};
Foo foo = Foo( 1.0, vec3(2.0) );
Pour initialiser un tableau de structures on peut écrire, de façon assez verbeuse :
Foo foo[3] = Foo[3]( Foo( 1.0, vec3(2.0) ),
Foo( 3.0, vec3(4.0) ),
Foo( 5.0, vec3(6.0) ) );
À partir de GLSL 4.2+ on peut aussi employer la syntaxe des listes d'initialisation du C++11 :
Foo foo[3] = { { 1.0, {2.0} },
{ 3.0, {4.0} },
{ 5.0, {6.0} } };
Les listes d'initialisation sont valables sur tous les types :
Foo foo = { 1.0, {2.0, 3.0} };
vec2 v = {1.0, 2.0};
float pi = {3.14};
7.2.5. Qualificatifs de type
Toute variable a une portée, comme en C/C++ ; une variable déclarée en dehors d'une fonction est globale au shader.
Les variables peuvent recevoir un qualificatif de type,
pour indiquer la manière dont la variable doit être gérée ou stockée.
Par exemple, const
indique que la variable ne peut pas changer de valeur
après son initialisation.
Les variables globales peuvent recevoir des qualificatifs pour spécifier
les entrées-sorties du shader : in
, out
, uniform
, etc.
-
Les globales
in
reçoivent une donnée du client (vertex shader) ou de l'étape précédente dans le pipeline, possiblement interpolée (fragment shader). Ces variables, bien que n'étant pasconst
, ne peuvent pas être modifiées par le shader. -
Les globales
out
fournissent une donnée pour la suite du pipeline. Toutes les variablesout
doivent posséder une valeur à la fin du shader, sauf cas particuliers (par exemple un fragment shader faisant undiscard
). -
Les globales
in
ouout
peuvent en général être de n'importe quel type, sauf desstruct
et des types opaques. Il y a des limitations supplémentaires selon les shaders. -
Dans les premières versions de GLSL, les mots clés employés étaient
attribute
etvarying
.
Les globales uniform
sont passées par le programme client à tous les shaders,
et sont stockées dans le shader program :
-
Elles ne peuvent pas être données aux shader par le client OpenGL avant que le program shader ne soit lié.
-
Elles peuvent être de n'importe quel type, y compris un
struct
ou un type opaque. -
Elles ne peuvent pas être modifiées par un shader ; toutefois elles peuvent avoir une valeur par défaut dans le shader, qui sera écrasée par la valeur donnée par le client.
Il existe de nombreux autres qualificateurs, par exemple
des qualificateurs pour préciser l'interpolation
(flat
, noperspective
, smooth
, centroid
, sample
).
Il existe également des interface block
qui sont des regroupements de
variables d'entrées, de sorties, de variables uniform
et de buffers de stockage.
7.2.6. Qualificatifs de disposition
Les layout qualifiers permettent de préciser la disposition (le stockage) en mémoire des variables, ainsi que d'autres propriétés. La syntaxe est
layout(qualifier1, qualifier2 = value, ...) variable_definition
Le layout
permet en particulier de préciser dans les vertex shaders
pour les variables in
(également appelées attributes)
leur location
(encore appelée attribute index).
Par exemple si dans le vertex shader on déclare :
layout(location = 1) in vec3 vPos;
alors dans le programme OpenGL, au lieu d'écrire
Guint vPos_loc = glGetAttribLocation (m_program, "vPos");
glVertexAttribPointer (vPos_loc, 3, GL_FLOAT, ...);
glEnableVertexAttribArray (vPos_loc);
on pourra directement écrire
glVertexAttribPointer (1, 3, GL_FLOAT, ...);
glEnableVertexAttribArray (1);
Remarque : il est aussi possible de fixer la location d'un vertex attribute
depuis le programme client avec glBindAttribLocation
; mais en cas
de contradiction avec la déclaration du layout
du shader, c'est toujours
le layout
qui gagne.
Certains types de données peuvent "consommer" plusieurs locations, par exemple si on déclare
layout(location = 2) in vec3 values[4];
alors les locations utilisées seront 2, 3, 4, 5, donc la prochaine location
disponible sera 6.
Le nombre de locations est limité et architecture-dépendant ;
la limite ne peut être consultée, mais elle est
\(\geqslant\) GL_MAX_VARYING_COMPONENTS/4
\(\geqslant\) 60/4
,
voir glGet
.
Certains types n'occupent pas toute la place dans une location, et il est possible
de regrouper des variables en utilisant le mot-clé component
dans le layout
.
On peut préciser avec le layout
la location et plein d'autres propriétés
pour toutes sortes d'objets et de shaders, les variables out
, les uniform
,
etc, voir la documentation.
7.2.7. Variables prédéfinies
De nombreuses variables sont prédéfinies et sont spécifiques à chaque type de shader, voir builtin variables. De nombreuses builtin des premières versions de GLSL ont été dépréciées, et remplacées par de nouvelles (qui n'ont rien à voir).
Les vertex shaders possèdent en entrée les variables :
in int gl_VertexID;
l'indice du sommet courant ;in int gl_InstanceID;
le numéro d'instance dans le cas de dessins multiples.- etc.
Et en sortie :
out vec4 gl_Position;
la position du sommet ;out float gl_PointSize;
la taille du point pour le dessin deGL_POINTS
.- etc.
Les fragment shaders ont les variables suivantes en entrée :
in vec4 gl_FragCoord;
les coordonnées du fragment par rapport à la fenêtre. Les coordonnéesz
etw
ont une signification particulière. L'origine des coordonnées peut être modifiée par unlayout
.in bool gl_FrontFacing;
estfalse
si le fragment est situé sur la face arrière d'une primitive ;- etc.
Et en sortie :
out float gl_FragDepth;
la profondeur du fragment pour le Z-buffer ; écrase la valeur degl_FragCoord.z
.- etc.
7.2.8. Fonctions standard
De nombreuses fonctions sont fournies :
- trigonométrie :
sin
,cos
,atan
... - exponentielle :
exp
,log
,log2
... - math:
sqrt
,abs
,sign
,floor
... min
,max
,clamp
,mix
...- géométrie :
length
,distance
,dot
,cross
... - matrices :
*
,transpose
...
Voir les pages de référence (à gauche cliquer sur accordion style puis GLSL).
Pour aller plus loin :
- le Wiki de Khronos ;
- le Book of shaders.
7.3. Exemples
7.3.1. Fichiers de shaders
Pour tester le langage GLSL on se donne l'exemple
fw41-file.cpp
,
qui peut charger des fichiers de shaders via la ligne de commande,
et recharger les fichiers de shaders avec la touche U
(pour update).
L'usage est :
$ ./fw41-file [-vs vertex.glsl] [-fs fragment.glsl]
Comme point de départ on peut prendre les fichiers
vs01.glsl
et fs01.glsl
.
Voici quelques explications sur l'évolution du code par rapport au dernier exemple du CM 03.
On passe argc
et argv
au constructeur de MyApp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
On analyse la ligne de commande dans la méthode parse_args
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Elle modifie les données membres, par défaut vides :
std::string m_vertex_shader_path, m_fragment_shader_path;
Dans initGL
, tout le code relatif aux shaders est remplacé par :
1 2 3 4 5 6 7 |
|
La méthode load_and_compile_program
appelle load_shader_code
, puis
compile les shaders, lie le shader program et renvoie son identifiant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
La méthode load_shader_code
reçoit le chemin du fichier, et en second
paramètre le texte du shader par défaut, puis renvoie le code chargé si le chemin
est non vide et la lecture a réussi, sinon le code par défaut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Les shaders par défaut sont déclarés dans la classe MyApp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Enfin, l'appel de la touche U
déclenche la méthode load_and_compile_program
:
1 2 3 4 5 6 |
|
Exemples de manipulations
En modifiant les shaders vs01.glsl
ou fs01.glsl
puis en appuyant sur le touche U
pour les recharger :
- remplacer la couleur par une constante ;
- multiplier la couleur par un scalaire dans l'un des deux shaders ;
- changer les couleurs avec le swizzling ;
- enlever la multiplication avec la matrice
MVP
; - utiliser
gl_FrontFacing
pour la couleur avant ou arrière (exemplefs02.glsl
) ; - supprimer le fragment avec
discard
pour la face arrière ; - etc.
7.3.2. Position de la souris
L'exemple fw42-mouse.cpp
récupère la position
de la souris et la taille de la fenêtre, et mémorise le tout dans une
variable uniform vec4 mousePos
sous la forme (mouse_x
, mouse_y
, width
,
height
).
Le but est de pouvoir faire des effets de couleurs selon la position de la souris,
et en utilisant le fait que les coordonnées d'un fragment sont accessibles
dans gl_FragCoord
.
On rajoute les données membres suivantes dans MyApp
:
vmath::vec4 m_mousePos; // mouse_x, mouse_y, width, height
GLint m_mousePos_loc;
puis on récupère la location de l'uniform dans le shader program :
m_mousePos_loc = glGetUniformLocation (m_program, "mousePos");
Pour chaque rendu,
on mémorise dans la méthode displayGL
la variable uniform
si la location est trouvée :
if (m_mousePos_loc != -1)
glUniform4fv (m_mousePos_loc, 1, m_mousePos);
On rajoute également ce test à la fin de reload_program
car d'une part,
la situation a pu changer, et d'autre part, les variables uniform
s sont
mémorisées dans le shader program, or il vient d'être reconstruit.
Quant à m_mousePos
, ce vecteur vmath::vec4
est peuplé par une nouvelle callback :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
qui est appelée à chaque déplacement de la souris, et
qui est enregistrée dans le constructeur de MyApp
par :
glfwSetCursorPosCallback (m_window, on_mouse_func);
Exemples de manipulations
Pour ces différents exemples on conserve le vertex shader
vs01.glsl
.
Avec le fragment shader fs03.glsl
,
la couleur générale varie selon la position de la souris dans la fenêtre :
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Dans l'exemple de fragment shader fs04.glsl
,
les couleurs varient selon la position du fragment dans la fenêtre :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Enfin dans le fragment shader fs05.glsl
on
dessine des ondes autour de la souris, en calculant la distance euclidienne
d
entre le fragment et la souris, puis en multipliant les
couleurs par sin(d)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
8. Textures
Une texture est une image en 1, 2 ou 3 dimensions, qui peut être stockée dans la mémoire du serveur puis "plaquée" sur des primitives graphiques.
Les textures permettent également de stocker des données arbitraires.
Dans la suite on s'intéresse principalement aux textures 2D.
8.1. Chargement des images
Il y a de très nombreux formats d'image, et OpenGL ne dispose pas de fonctions pour les charger, aussi on a besoin d'une librairie externe.
Dans ce cours nous utiliserons la librairie "tout-en-un-fichier"
stb_image.h
,
qui permet de lire les formats d'images principaux
(mais pas de les écrire).
Le module stb_image.c
sera obtenu simplement en écrivant
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
Pour charger une image, il suffit alors d'écrire :
#include "stb_image.h"
int width, height, n_comp;
unsigned char *data = stbi_load ("my-image.png", &width, &height, &n_comp, 0);
Les variables width
et height
seront remplies avec la taille de l'image lue,
et n_comp
par le nombre de canaux de l'image.
En cas d'échec, la valeur renvoyée sera NULL
; on peut alors obtenir un
message d'erreur en invoquant stbi_failure_reason()
.
Pour libérer l'image il suffit d'appeler stbi_image_free (data)
.
8.2. Génération d'une texture
On commence par générer un identifiant de texture avec glGenTextures
,
puis on rend la texture courante avec glBindTexture
en la liant
au contexte OpenGL.
C'est l'occasion de préciser le type de texture (target) que l'on souhaite, ici
GL_TEXTURE_2D
:
GLuint texture_id;
glGenTextures (1, &texture_id);
glBindTexture (GL_TEXTURE_2D, texture_id);
Toutes les opérations sur textures que l'on fera concerneront cette texture, jusqu'à ce qu'on rende active une autre texture (ou 0 pour désactiver la texture courante).
Chaque fois que l'on aura besoin de cette texture, il suffira de rappeler
glBindTexture
, avec la même target et l'identifiant.
Maintenant que l'image est chargée dans le tableau data
, on peut générer
une texture 2D en invoquant glTexImage2D
:
glTexImage2D (GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, data);
C'est cette étape qui mémorise l'image dans la mémoire du serveur.
L'étape suivante consiste à invoquer glGenerateMipmap
pour générer un mipmap (Multum In Parvo MAPping) ;
un mipmap est une collection de copies de l'image à différentes résolution, et qui permet
d'afficher une texture plus efficacement selon la taille relative dans la fenêtre.
glGenerateMipmap (GL_TEXTURE_2D);
On peut enfin libérer data
(puisque la texture est mémorisée dans la mémoire
du serveur) en appelant
stbi_image_free (data);
La texture est maintenant prête à être utilisée.
Pour résumer, voici une méthode qui fait tout le travail : elle prend en paramètre le chemin d'une image, la charge, crée une texture 2D puis renvoie son ID :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
8.3. Exemple des triangles
Dans l'exemple fw43-texture.cpp
nous
chargeons deux textures, puis elles sont plaquée chacune sur un triangle,
dans une nouvelle version de la classe Triangles
.
Les images sont chargées avec la méthode load_texture
de la classe MyApp
,
présentée dans la section précédente.
Les identifiants des textures sont mémorisés dans m_texture_id1
et m_texture_id2
:
class MyApp
{
...
GLuint m_texture_id1, m_texture_id2;
const char* m_texture_path1 = "side1.png";
const char* m_texture_path2 = "side2.png";
void initGL()
{
...
// Création des textures
m_texture_id1 = load_texture (m_texture_path1);
m_texture_id2 = load_texture (m_texture_path2);
...
Par la suite dans le vertex shader, on recevra en entrée les coordonnées de texture
du sommet courant dans la variable vTex
.
On mémorise dès à présent sa location m_vTex_loc
dans initGL
, de manière à
la transmettre à l'instanciation de la classe Triangles
:
class MyApp
{
...
GLint m_vPos_loc, m_vTex_loc;
void initGL()
{
...
m_program = load_and_compile_program (...)
// Récupère l'identifiant des "variables" dans les shaders
m_vPos_loc = glGetAttribLocation (m_program, "vPos");
m_vTex_loc = glGetAttribLocation (m_program, "vTex");
// Création des objets graphiques
m_triangles = new Triangles {m_vPos_loc, m_vTex_loc};
...
L'instance m_triangles
de la classe Triangles
est libérée dans tearGL
; on y détruit
également les textures :
void tearGL()
{
// Destruction des objets graphiques
delete m_triangles;
...
glDeleteTextures (1, &m_texture_id1);
glDeleteTextures (1, &m_texture_id2);
glDeleteProgram (m_program);
}
Pour afficher les triangles dans displayGL
, on a juste besoin de passer
les identifiants des textures :
void displayGL()
{
...
// Dessins
m_triangles->draw (m_texture_id1, m_texture_id2);
8.4. Rendu avec une texture
Passons au rendu dans la classe Triangles
.
Les données sont mémorisées dans deux tableaux : dans positions
on
mémorise les coordonnées 3D des sommets dans l'espace, et dans tex_coords
les coordonnées 2D des sommets dans la texture.
Triangles (...)
{
// Données
GLfloat positions[] = {
-0.7, -0.5, -0.1,
...
};
GLfloat tex_coords[] = {
0.0, 1.0,
...
};
Le principe des coordonnées de texture est le suivant : indépendamment de la taille en pixels de l'image originale, le coin inférieur gauche de l'image a les coordonnées de texture (0.0, 0.0), et le coin supérieur droit a les coordonnées de texture (1.0, 1.0).
La texture sera "plaquée" par le vertex shader sur les primitives graphiques à partir des coordonnées de texture de chaque sommet, indépendamment de la position des sommets dans l'espace, et les points à l'intérieur des primitives graphiques seront obtenus par le fragment shader en interpolant les coordonnées pour aller chercher la couleur du pixel le plus proche dans la texture.
Pour transmettre les 3+2 coordonnées de chaque sommet on fabrique un vecteur avec les données à la suite :
// Création d'une structure de données à plat
std::vector<GLfloat> vertices;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 3; j++)
vertices.push_back (positions[i*3+j]);
for (int j = 0; j < 2; j++)
vertices.push_back (tex_coords[i*2+j]);
}
...
Ensuite on copie les données dans le VBO courant à l'aide de 2 VAA, un par
variable in
de shader.
Notez les offsets, et le stride de 5 GLfloat
utilisés :
Triangles (GLint vPos_loc, GLint vTex_loc)
: m_vPos_loc {vPos_loc}, m_vTex_loc {vTex_loc}
{
// Données, données à plat ...
// Création du VAO ...
// Création du VBO ...
// VAA associant les données à la variable vPos du shader, avec l'offset 0
glVertexAttribPointer (m_vPos_loc, 3, GL_FLOAT, GL_FALSE,
5*sizeof(GLfloat), reinterpret_cast<void*>(0*sizeof(GLfloat)));
glEnableVertexAttribArray (m_vPos_loc);
// VAA associant les données à la variable vTex du shader, avec l'offset 3
glVertexAttribPointer (m_vTex_loc, 2, GL_FLOAT, GL_FALSE,
5*sizeof(GLfloat), reinterpret_cast<void*>(3*sizeof(GLfloat)));
glEnableVertexAttribArray (m_vTex_loc);
Pour faire le rendu on a juste besoin des identifiants de texture, pour rendre active la bonne texture avant le dessin de chaque triangle :
void draw (GLuint texture_id1, GLuint texture_id2)
{
glBindVertexArray (m_VAO_id);
glBindTexture (GL_TEXTURE_2D, texture_id1);
glDrawArrays (GL_TRIANGLES, 0, 3);
glBindTexture (GL_TEXTURE_2D, texture_id2);
glDrawArrays (GL_TRIANGLES, 3, 3);
glBindVertexArray (0);
}
8.5. Shaders pour texture
On s'attaque enfin aux shaders.
Dans l'exemple fw43-texture.cpp
,
les deux shaders ci-dessous sont déjà les shaders par défaut, donc vous
pouvez directement exécuter le programme sans fournir de shader via
les options de ligne de commande -vs
et -fs
.
Vertex shader
Dans le vertex shader on reçoit en entrée dans la variable in vec2 vTex
les coordonnées de texture du sommet courant ; on se contente de les
recopier dans la variable en sortie out vec2 texCoord
.
Le nom de la variable en sortie n'a pas d'importance, il suffit d'utiliser
la même variable dans le fragment shader (comme pour toutes les variables out
)
pour que sa valeur soit transmise, interpolée, dans le fragment shader pour
chaque fragment.
#version 330
in vec4 vPos;
in vec2 vTex;
out vec2 texCoord;
uniform mat4 matMVP;
void main()
{
gl_Position = matMVP * vPos;
texCoord = vTex;
}
Fragment shader
C'est ici que l'on va déterminer la couleur du fragment en allant "piocher" dans la texture.
La coordonnée de texture interpolée est reçue dans la variable in vec2 texCoord
.
La texture est mémorisée dans un type opaque appelé sampler2D
,
accessible en lecture seule dans les shaders sous la forme d'une variable uniform
.
Il n'y a pas besoin de chercher le nom de la variable dans le shader program,
il suffit de déclarer la variable uniform
avec le type sampler2D
, et
le linker OpenGL établit la correspondance.
(Il peut y avoir 2 textures accessibles en même temps mais il faut alors
utiliser des layout(location=...)
explicites).
On récupère ensuite la couleur dans la texture uTex
tout simplement en
la lui demandant, au moyen de la fonction dédiée texture2D
, en lui appliquant
les coordonnées texCoord
:
#version 330
in vec2 texCoord;
out vec4 fragColor;
uniform sampler2D uTex;
void main()
{
fragColor = texture2D (uTex, texCoord);
}
Exemples de manipulations
Enregistrer le fragment shader dans un fichier, puis le passer
en ligne de commande avec l'option -fs
avec les variantes :
fragColor = texture2D (uTex, texCoord*0.5);
fragColor = texture2D (uTex, texCoord*3.0);
fragColor = texture2D (uTex, vec2(texCoord.s*2.0, texCoord.t*3.5));
fragColor = texture2D (uTex, texCoord.ts);
fragColor = texture2D (uTex, texCoord)*0.5;
fragColor = clamp(texture2D (uTex, texCoord)*1.5, 0.0, 0.8);
La manière d'afficher la texture lorsque les coordonnées dépassent de l'intervalle
\([0,\ldots 1]\) peut être configuré à l'aide la la fonction
glTexParameter
et les paramètres GL_TEXTURE_WRAP_S
et GL_TEXTURE_WRAP_T
,
voir des exemples dans learnopengl.