IV. Étude des shaders▲
IV-A. Techniques de base▲
IV-A-1. Éclairage▲
L'éclairage est une partie importante pour toute application graphique. Nous allons reproduire le comportement du pipeline fixe lors du calcul des lumières.
Pour rappel, nous distinguons trois types de sources :
- La lampe directionnelle ;
- Le spot ou projecteur (voyons-le comme un cône ayant sa pointe à la source de la lampe) ;
- Le point de lumière (point light) (pensez à une luciole).
Lors du calcul de l'éclairage, nous distinguons trois types de lumière :
-
la lumière ambiante représentant une lumière globale ;
-
la lumière diffuse représentant les rayons rebondissant sur nos objets des différentes sources de lumière ;
- la lumière spéculaire représentant le reflet des lumières.
Avant de commencer les calculs, il faut déterminer les besoins afin de décrire les lumières. Les informations à passer à notre shader sont les suivantes :
- la position de la lumière. Effectivement, avec la position de la lumière, il sera plus facile de calculer la zone éclairée par la lumière ;
- la couleur ambiante de la lumière ;
- la couleur diffuse de la lumière ;
- la couleur spéculaire de la lumière.
Seulement pour les spots et les points de lumière :
- atténuation constante ;
- atténuation linéaire ;
- atténuation quadratique.
Seulement pour les spots :
- une direction ;
- un angle d'ouverture ;
- un coefficient d'exposition (un objet en plein dans l'angle du spot sera plus éclairé qu'un objet à la limite de la zone éclairée par le spot) ;
- une limite d'éclairage (pour définir là où le spot n'éclaire plus, car l'objet est trop loin).
Il est toujours possible de ne pas utiliser toutes ces informations afin de simplifier et/ou d'optimiser les calculs.
Pour les exemples de code qui suivent, nous allons partir du principe que nous utilisons la structure suivante :
struct
Lumiere
{
vec3 position;
vec4 couleurAmbiante;
vec4 couleurDiffuse;
vec4 couleurSpeculaire;
float
shininess ; // Seulement pour la lumière spéculaire
// Spécifique aux spots et points
float
attenuationConstante ;
float
attenuationLineaire ;
float
attenuationQuadratique ;
// Spécifique aux spots
vec3 direction ;
float
angle ;
float
exposition;
float
limite;
}
Afin de faire les calculs de lumière diffuse et de lumière spéculaire, nous avons besoin des normales pour chaque vertex de nos objets. Finalement, il est nécessaire de connaître l'inverse de la matrice de vue, pour le calcul de la lumière spéculaire.
IV-A-1-a. La lumière ambiante▲
Certainement la plus simple à appliquer, celle-ci est simplement une application de la couleur ambiante sur l'objet à éclairer.
couleurAmbiante =
lumiere.ambiant;
IV-A-1-b. La lumière diffuse▲
La lumière diffuse est une application de la formule de la réflexion de Lambert :
- intensité = couleur_de_la_lumière_diffuse * cos(angle_entre_le_rayon_de_la_lumière_et_la_normale_de_l'objet)
Comme vous pouvez le constater, la couleur que l'on applique sur le pixel n'est qu'une fraction de la lumière de la lampe. La fraction dépend de l'angle entre le rayon lancé par la lampe sur l'objet et de l'orientation de l'objet (sa normale).
Le rayon de la lumière n'est autre que le vecteur entre la lumière et le point d'impact de celle-ci. Il est simple de connaître ce vecteur, car nous avons la position de la source de lumière, mais aussi la position du point de rebond. Ce point est exactement celui dont nous calculons l'intensité de la lumière. L'opération sera donc :
Vec3 rayon =
lumiere.position &
#150
; vertex;
(Note : la position de la lumière doit être normalisée.)
Pour connaître le cosinus de l'angle entre deux vecteurs, nous allons utiliser le produit scalaire (en anglais : dot product). Le GLSL nous fournit une fonction appelée 'dot' pour appliquer ce calcul. Nous aurons donc dans notre code :
float
angleLumiere =
dot
(
normal, rayon);
Finalement, nous pouvons appliquer la formule de la réflexion de Lambert :
Vec3 couleurDiffuse =
lumiere.diffuse *
angleLumiere;
IV-A-1-c. La lumière spéculaire▲
La lumière spéculaire est le reflet de la lampe sur l'objet que nous dessinons. Pour calculer celle-ci, nous utilisons le modèle simplifié Blinn-Phong. La formule de ce modèle est la suivante :
- spec = (Normale . (œil rayon_lumière))shininess * couleur_de_la_lumière_speculaire
Pour connaître la position de l'œil, nous allons utiliser la position du vertex que nous sommes actuellement en train de calculer pour lui appliquer la matrice inverse de vue. Pour rappel, la matrice de vue, permet de transformer des coordonnées de l'espace monde à l'espace de la caméra. La multiplication avec la matrice inverse fait donc l'opération inverse, ainsi, nous pouvons retrouver la position de la caméra. Le code de cette opération est le suivant :
Vec3 oeil =
matrice_inverser_vue *
vertex;
Le rayon_lumière, qui représente le vecteur entre la lumière et l'objet se calcule en soustrayant la position de la lumière à celle du vertex :
Vec3 ray =
lumiere.position &
#150
; vertex;
Le reste de la formule ne pose pas de problème. Pour rappel, afin d'effectuer les calculs sur les puissances le GLSL fournit la fonction pow (pour 'power' qui signifie puissance en anglais). La variable appelée 'shininess' correspond à la force du reflet.
vec3 couleurSpeculaire =
pow
(
dot
(
normale,(
oeil-
ray)),lumiere.shininess) *
lumiere.couleurSpeculaire;
Voici les résultats avec un shininess de 8, 64 et 128 :
IV-A-1-d. Application de nos différentes lumières▲
Le calcul final de la couleur se résume à la somme des différentes lumières. Toutefois, la lumière spéculaire ne doit pas apparaître si l'angle de la lumière (défini lors du calcul de la lumière diffuse) est inférieur à zéro (il serait dommage de voir le reflet de la lampe alors que celle-ci éclaire l'autre face de l'objet).
gl_FrontColor =
couleurAmbiante +
couleurDiffuse +
couleurSpeculaire;
Maintenant que nous savons comment calculer les différentes lumières, nous allons découvrir comment reproduire les différents types de lampes. Les formules que nous avons apprises juste ci-dessus sont appliquées dans les trois cas, seulement des effets d'atténuation, ou de limite sont ajoutés.
IV-A-1-e. Lampe directionnelle▲
La lampe directionnelle peut être comparée au soleil. Ainsi nous pouvons prétendre que la lumière est à une distance infinie, ce qui nous permet de considérer tous les rayons parallèles et ne subissant aucune atténuation. Pour l'appliquer, nous n'avons besoin que de la direction de ses rayons (ou de la position de la lampe). Cela nous simplifie énormément les calculs.
En fait, comme il s'agit d'une lampe sans atténuation, qui éclaire le monde entier, les calculs de la lumière peuvent être effectués tels qu'ils ont été présentés précédemment.
IV-A-1-f. Point de lumière▲
Cette lampe ne se base que sur la distance des objets. Selon cette distance, l'objet sera plus ou moins éclairé. De plus, cette lampe éclaire dans toutes les directions.
Comme vous avez pu le deviner, il nous faut calculer la distance de la lampe à l'objet :
Vec3 vLumObj =
lumiere.position &
#150
; vertex ;
float
distance =
length
(
vLumObj);
Puis, encore une fois, nous allons calculer l'atténuation selon cette distance :
float
attenuation =
lumiere.attenuationConstante +
lumiere.attenuationLineaire *
d +
lumiere.attenuationQuadratique *
d *
d;
Finalement, nous n'avons plus qu'à appliquer cette atténuation sur la lumière :
couleurAmbiante =
couleurAmbiante *
attenuation ;
couleurDiffuse =
couleurDiffuse *
attenuation ;
couleurSpeculaire =
couleurSpeculaire *
attenuation;
IV-A-1-g. Spot▲
Le spot peut être représenté par un cône ayant la pointe à la source de la lampe. Si l'objet éclairé est au centre du cône, il reçoit la lumière « forte », mais si celui-ci est sur les bords, la couleur de la lumière est atténuée.
Déjà vous pouvez vous douter des besoins d'une telle lampe. Il nous faut :
- la direction de la lumière ;
- l'angle d'ouverture du cône ;
- atténuation constante ;
- atténuation linéaire ;
- atténuation quadratique ;
- une limite d'éclairage ;
- un coefficient d'exposition.
Commençons par récupérer la distance entre notre lampe et notre objet. Lors du calcul du rayon de la lumière, nous avons vu comment récupérer le vecteur entre la lumière et l'objet. Pour calculer la distance à partir d'un vecteur, le GLSL nous fournit une fonction toute prête : length() (qui signifie 'distance' en anglais).
Vec3 vLumObj =
lumiere.position &
#150
; vertex ;
float
distance =
length
(
vLumObj);
Maintenant, nous allons calculer l'atténuation selon la distance de l'objet. Pour cela, nous allons reprendre la formule utilisée par OpenGL dans son pipeline fixe.
float
attenuation =
lumiere.attenuationConstante +
lumiere.attenuationLineaire *
d +
lumiere.attenuationQuadratique *
d *
d;
Ensuite nous devons savoir si l'objet est dans le cône de lumière de la lampe. Pour cela nous allons calculer l'angle entre le vecteur de la position de la lampe à l'objet et le vecteur entre l'objet et la direction de la lampe. Encore une fois, nous utilisons le produit scalaire :
float
angle =
dot
(-
vLumObj, normalize
(
lumiere.direction));
Nous allons calculer une deuxième atténuation qui, cette fois, dépend de l'angle entre le spot et l'objet.
float
spotAttenuation =
0
.0f
if
(
angle >=
lumiere.limite )
{
spotAttenuation =
pow
(
angle,lumiere.exposition);
}
Nous appliquons cette atténuation sur la première que nous avions trouvée :
attenuation *=
spotAttenuation;
Pour finir, nous appliquons l'atténuation dans notre calcul de la lumière :
couleurAmbiante =
couleurAmbiante *
attenuation ;
couleurDiffuse =
couleurDiffuse *
attenuation ;
couleurSpeculaire =
couleurSpeculaire *
attenuation;
IV-A-1-h. Matériaux▲
Il nous reste une toute petite partie (optionnelle) lors du calcul de l'effet de lumière sur notre objet. Actuellement, dans nos calculs de lumière nous n'avons pas pris en compte la couleur de nos objets. Plus précisément, nous parlons de matériaux, car les objets peuvent réfléchir de différentes manières la lumière.
Pour décrire un matériau, il faut définir :
- la couleur ;
- la couleur diffuse (celle qui est renvoyée à l'œil) ;
- la couleur spéculaire (une altération du reflet de la lumière, par le matériau).
Pour appliquer un matériau, il faut juste suivre la formule suivante :
(Je pars du principe, que les différentes couleurs contiennent les valeurs précédentes de nos calculs de lumière.) La structure pour les matériaux peut être la suivante :
struct
Materiaux
{
vec4 ambiante;
vec4 diffuse;
vec4 speculaire;
}
couleurAmbiante =
couleurAmbiante *
mat.ambiante;
couleurDiffuse =
couleurDiffuse *
mat.diffuse;
couleurSpeculaire =
couleurSpeculaire *
mat.speculaire;
Notez qu'encore une fois, il faut que la lumière spéculaire ne soit visible que si l'angle est supérieur à 0.
IV-A-1-i. Conclusion sur les lumières▲
Nous avons vu les différentes lumières et les différents types d'éclairages qui étaient présents dans le pipeline fixe d'OpenGL. Vous pouvez implémenter vos propres effets afin d'avoir le résultat qui vous convient le mieux. Le site Wikipedia regorge d'informations sur ce sujet en perpétuelle évolution.
Les algorithmes décrits dans cette section peuvent être appliqués dans le vertex shader ou dans le pixel shader. Sachant que le pixel shader est exécuté pour tous les pixels dessinés, l'exécution sera plus lente, par contre, ce sera plus beau.
L'ancien pipeline était limité à huit lampes. Maintenant vous pouvez faire plus, mais cela va ralentir l'exécution. Il existe une méthode appelée « rendu différé » (Deferred Shading) permettant de mettre des centaines de lampes dans une scène.
IV-A-2. Application d'une texture▲
Du côté OpenGL, le code pour appliquer une texture est le même que celui qui n'utilise pas de shader.
Pour les textures, il y a quatre variables à gérer :
- la matrice de la texture : gl_TextureMatrix ;
- les coordonnées de la texture : gl_MultiTexCoord0 ;
- les coordonnées de la texture (interpolées) : gl_TexCoord[ gl_MaxTextureCoords ] ;
- l'échantillon de la texture (les couleurs), qui est une variable uniforme.
La matrice de texture est un tableau, ayant au maximum gl_MaxTextureCoords. gl_MultiTexCoord0 représente les coordonnées de la première texture, pour le vertex actuel. Le zéro peut être remplacé par un chiffre de 0 à 7.
L'échantillon 'sampler' est un type qui représente la texture dans le fragment shader.
Ce qui nous donne le processus suivant :
Vertex shader : transforme nos coordonnées de texture et passe le résultat au fragment shader.
void
main (
void
)
{
gl_TexCoord[0
] =
gl_TextureMatrix[0
] *
gl_MultiTexCoord0;
gl_Position =
ftransform
(
);
//gl_FrontColor = vec4(couleur,1.0);
}
Fragment shader : accède à notre texture pour récupérer la couleur. Pour cela, nous utilisons texture2D, qui nous retournera la couleur de la texture à l'endroit indiqué par les coordonnées de texture.
uniform sampler2D tex;
void
main
(
void
)
{
gl_FragColor =
texture2D
(
tex,gl_TexCoord[0
].st);
}
IV-A-2-a. Différents types d'application▲
Dans le pipeline fixe d'OpenGL, nous avions la possibilité de choisir la façon dont la couleur de la texture allait être appliquée sur notre objet. Je vais retranscrire rapidement les différentes formules.
GL_REPLACE :
Remplace la couleur actuelle par la couleur de la texture. C'est exactement le calcul que j'ai fait dans l'exemple précédent.
Couleur =
texture2D
(
tex0, gl_TexCoord[0
].xy) ;
GL_MODULATE :
Multiplie la couleur venant du vertex shader avec celle de la texture. Cela peut être utile si le calcul de la lumière est effectué dans le vertex shader, et que cette lumière doit avoir un effet sur l'application de la texture.
couleur *=
texture2D
(
tex0, gl_TexCoord[0
].xy) ;
GL_DECAL :
Utile dans le cas où vous voulez afficher la texture d'un logo sur une surface. La valeur alpha de la texture est utilisée pour faire une interpolation entre la couleur venant du vertex shader (gl_Color), et la couleur de la texture. Finalement, la valeur alpha du vertex shader est utilisée.
Vec4 couleurText =
texture2D
(
tex0, gl_TexCoord[0
].xy) ;
vec3 coul =
mix
(
color.rgb, couleurText.rgb, couleurText.a) ;
couleur =
vec4
(
coul, color.a);
GL_BLEND :
Ce mode prend en compte la couleur de la texture de l'environnement, en faisant une interpolation entre la couleur venant du vertex shader et celle-ci. Le facteur pour l'interpolation est la texture. Finalement la couleur alpha est déterminée par la multiplication de l'alpha entrant et celui de la texture.
Vec4 couleurText =
texture2D
(
tex0, gl_TexCoord[0
].xy) ;
vec3 coul =
mix
(
color.rgb, gl_TextureEnvColor[0
].rgb, couleurText.rgb) ;
couleur =
vec4
(
coul, color.a *
couleurText.a);
GL_ADD :
Ajoute simplement les couleurs. La composante alpha est multipliée pour calculer la composante alpha résultante. Finalement nous remettons les valeurs dans l'intervalle [0, 1], car le calcul peut engendrer des résultats trop grands.
Vec4 couleurText =
texture2D
(
tex0, gl_TexCoord[0
].xy) ;
color.rgb *=
couleurText.rgb;
color.a *=
couleurText.a ;
couleur =
clamp
(
color,0
.0
, 1
.0
);
Depuis OpenGL 1.3, il y a de nouveaux modes d'application de texture. Ils ne sont pas décrits ici, car cela sort du cadre de ce tutoriel. De plus, il est facile de deviner leur implémentation selon leur description.
IV-A-2-b. Génération automatique de coordonnées de texture▲
Dans OpenGL, nous pouvions demander au pipeline de générer automatiquement les coordonnées de texture. Si vous voulez toujours utiliser cette méthode (qui présente quelques défauts), voici comment faire.
Pour rappel, OpenGL définit cinq types de générations :
- GL_OBJECT_LINEAR : utile lorsque la texture est fixée à l'objet (par exemple : un terrain) ;
- GL_EYE_LINEAR : utile pour produire des contours dynamiques aux objets ;
- GL_SPHERE_MAP : peut générer des coordonnées pour l'application d'environnement ;
- GL_REFLECTION_MAP : utilise le vecteur de réflexion comme coordonnées de texture ;
- GL_NORMAL_MAP : utilise la normale comme coordonnées.
Le calcul pour une sphère, tel qui est décrit dans la spécification d'OpenGL est le suivant :
vec2 sphereMap
(
in vec3 cameraPosition, in vec3 normal)
{
float
m ;
vec3 r, u ;
u =
normalize
(
cameraPosition) ;
r =
reflect
(
u, normal) ;
m =
2
.0
*
sqrt
(
r.x *
r.x +
r.y *
r.y +
(
r.z +
1
.0
) *
(
r.z +
1
.0
)) ;
return
vec2
(
r.x /
m +
0
.5
, r.y /
m +
0
.5
);
}
Le calcul utilisé pour GL_REFLECTION_MAP est :
vec3 reflectionMap
(
in vec3 cameraPosition, in vec3 normal)
{
vec3 u =
normalize
(
cameraPosition) ;
return
(
reflect
(
u,normal));
}
Finalement, selon le choix que vous faites pour votre génération, le code généraliste pour avoir les coordonnées de texture est le suivant :
vec2 sphereMapCoord ;
vec3 reflectionCoord;
If (
isTexSphere )
{
sphereMapCoord =
sphereMap
(
cameraPosition,normal);
}
if
(
isTexReflection )
{
reflectionCoord =
reflectionMap
(
cameraPostion,normal);
}
// Pour toutes les textures activées
for
(
int
i =
0
; i <
nombreTexture ; i++
)
{
if
(
isTexObject )
{
gl_TexCoord[i].s =
dot
(
gl_Vertex, gl_ObjectPlaneS[i]) ;
gl_TexCoord[i].t =
dot
(
gl_Vertex, gl_ObjectPlaneT[i]) ;
gl_TexCoord[i].p =
dot
(
gl_Vertex, gl_ObjectPlaneR[i]) ;
gl_TexCoord[i].q =
dot
(
gl_Vertex, gl_ObjectPlaneQ[i]);
}
if
(
isTexEye )
{
gl_TexCoord[i].s =
dot
(
gl_Vertex, gl_EyePlaneS[i]) ;
gl_TexCoord[i].t =
dot
(
gl_Vertex, gl_EyePlaneT[i]) ;
gl_TexCoord[i].p =
dot
(
gl_Vertex, gl_EyePlaneR[i]) ;
gl_TexCoord[i].q =
dot
(
gl_Vertex, gl_EyePlaneQ[i]);
}
if
(
isTexSphere )
{
gl_TexCoord[i] =
vec4
(
sphereMapCoord, 0
.0
, 1
.0
);
}
if
(
isTexReflection )
{
gl_TexCoord[i] =
vec4
(
reflectionCoord, 1
.0
);
}
if
(
isTexNormal )
{
gl_TexCoord[i] =
vec4
(
normal, 1
.0
);
}
}
IV-A-3. Brouillard▲
Le brouillard est un effet qui altère la couleur de l'objet selon la distance entre celui-ci et la caméra.
Dans le pipeline fixe, nous pouvions retrouver trois algorithmes différents pour le calcul du brouillard. Le premier pouvait être sélectionné en indiquant GL_LINEAR à glFogf(GL_FOG_MODE).
La formule utilisée est la suivante :
- f = fin z / fin début
Les variables doivent être dans l'espace de coordonnées de la vue. Toutes ces variables sont intégrées dans le GLSL :
début : gl_Fod.debut
fin : gl_Fod.end
z : gl_FogFragCoord
Le GLSL nous facilite quelque peu la tâche en nous proposant le résultat de gl_Fog.end gl_Fog.debut dans la variable gl_Fog.scale. Effectivement, ce calcul ne dépend pas de la position du vertex que nous sommes en train de calculer, donc le mieux est de ne faire qu'une seule fois le calcul.
La formule dans notre code sera donc :
Fog =
(
gl_Fod.end &
#150
; gl_FogFragCoord) *
gl_Fog.scale ;
Le deuxième brouillard présent dans OpenGL est un brouillard exponentiel donnant un résultat plus convaincant.
La formule utilisée est la suivante :
- f = e-(densité * z)
Z est toujours le même que précédemment (soit gl_FogFragCoord) et la densité est une variable uniforme donnée par le programme. Pour le calcul de l'exponentielle, nous allons utiliser la fonction exp() incluse dans le langage.
Le code résultant en GLSL est le suivant :
Fog =
exp
(-
gl_Fog.density *
gl_FogFragCoord) ;
Finalement, le troisième algorithme présent dans le pipeline fixe (correspondant à GL_EXP2) est une amélioration (graphique) de l'algorithme précédent.
La nouvelle formule est la suivante :
- fog = e-(densité * z)²
Qui correspond au code suivant :
Fog =
exp
(-
gl_Fog.density *
gl_FogFragCoord *
gl_Fog.density *
gl_FogFragCoord);
Pour appliquer le brouillard, il faut être sûr que la variable fog est entre 0 et 1. Cela peut se faire en utilisant une fonction du GLSL appelée clamp :
fog =
clamp
(
fog, 0
.0
,1
.0
);
Finalement, nous pouvons appliquer le brouillard sur la couleur calculée pour l'objet. Le brouillard a une couleur propre contenue dans gl_Fog.color :
couleur =
mix
(
vec3
(
gl_Fog.color), couleur, fog);
IV-A-4. Clipping▲
Le clipping est l'action de découper des morceaux de rendu final afin de ne pas les afficher. Cette fonctionnalité est toujours intégrée de manière fixe dans le pipeline graphique (entre le processeur de vertex et le processeur de fragment).
Si vous utilisez, le clipping, vous allez devoir indiquer la position de vertex afin qu'OpenGL puisse savoirs'il doit l'afficher ou non. Pour cela il suffit d'assigner une valeur à la variable gl_ClipVertex. Habituellement, les utilisateurs stockent le rectangle à découper dans l'espace de coordonnées de l'œil. Cela veut donc dire que nous devons transformer notre vecteur afin de l'avoir dans cet espace :
gl_ClipVertex =
gl_ModelViewMatrix *
gl_Vertex;