VI. Plus de lumières▲
Au cours des chapitres précédents, nous avons appris beaucoup sur l’éclairage dans OpenGL. Nous avons vu le modèle de Phong, les matériaux, les textures de lumière et différents types de lampes. Dans ce chapitre, nous allons combiner toutes ces connaissances en créant une scène entièrement éclairée au moyen de six sources lumineuses. On simulera la lumière du soleil avec une source directionnelle, quatre points lumineux répartis dans la scène, et nous ajouterons aussi une lampe frontale.
Pour utiliser plus d’une source de lumière dans la scène, nous allons effectuer les calculs d’éclairage dans des fonctions GLSL. En effet, le code devient vite très lourd si l’on effectue ces calculs, chaque lampe exigeant un calcul différent. Si l’on effectue ces calculs dans la fonction main(), le code devient inextricable.
Les fonctions en GLSL sont comme les fonctions en C. Nous avons un nom de fonction, un type pour la valeur de retour, et il faut déclarer un prototype au début du code si la fonction n’a pas été définie avant la fonction main(). Nous créerons une fonction par type de source de lumière : source directionnelle, point lumineux, spot.
Quand on utilise plusieurs sources dans une scène, l’approche est la suivante : on a un vecteur unique qui représente la couleur finale du fragment, et chaque source contribue à cette couleur finale. Ainsi, chaque source de la scène apportera sa contribution à la couleur finale. La structure générale du code ressemblera à cela :
out
vec4
FragColor;
void
main
(
)
{
// définition de la couleur finale
vec3
output =
vec3
(
0
.0
);
// ajout de la contribution des sources directionnelles
output +=
someFunctionToCalculateDirectionalLight
(
);
// idem pour les points lumineux
for
(
int
i =
0
; i <
nr_of_point_lights; i++
)
output +=
someFunctionToCalculatePointLight
(
);
// et enfin pour les spots ou autres sources
output +=
someFunctionToCalculateSpotLight
(
);
FragColor =
vec4
(
output, 1
.0
);
}
Le code réel sera légèrement différent selon les implémentations, mais la structure générale sera la même. On définit plusieurs fonctions qui calculent l’effet de chaque source et qui ajoutent la couleur résultante à la couleur finale. Si par exemple un fragment se trouve assez proche de deux sources, il sera plus clair que s’il n’est éclairé que par une seule source.
VI-A. Éclairage directionnel▲
Nous voulons définir une fonction dans le fragment shader qui calcule la contribution d’une source de lumière directionnelle sur le fragment : une fonction qui utilise des paramètres pour calculer et retourner la couleur de l’éclairage directionnel sur ce fragment.
Il nous faut déjà les variables que nous avions définies pour une source directionnelle. On peut placer ces variables dans une structure DirLight et la définir comme uniforme. Ces variables ont été vues au chapitre précédent :
struct DirLight {
vec3
direction;
vec3
ambient;
vec3
diffuse;
vec3
specular;
}
;
uniform
DirLight dirLight;
On peut ensuite passer la variable uniforme dirLight en paramètre d’une fonction avec le prototype suivant :
vec3
CalcDirLight
(
DirLight light, vec3
normal, vec3
viewDir);
Juste comme en C ou en C++, si nous voulons appeler une fonction (ici dans la fonction main()), la fonction doit être déclarée avant l’endroit où elle est appelée, ce qui est fait au moyen du prototype de la fonction. Nous placerons la définition de ces fonctions après la fonction main().
On voit que la fonction utilise une structure DirLight et deux vecteurs. Si vous avez suivi le tutoriel précédent, le code de cette fonction ne devrait pas vous surprendre :
vec3
CalcDirLight
(
DirLight light, vec3
normal, vec3
viewDir)
{
vec3
lightDir =
normalize
(-
light.direction);
// éclairage diffus
float
diff =
max
(
dot
(
normal, lightDir), 0
.0
);
// éclairage spéculaire
vec3
reflectDir =
reflect
(-
lightDir, normal);
float
spec =
pow
(
max
(
dot
(
viewDir, reflectDir), 0
.0
), material.shininess);
// assemblage du résultat
vec3
ambient =
light.ambient *
vec3
(
texture
(
material.diffuse, TexCoords));
vec3
diffuse =
light.diffuse *
diff *
vec3
(
texture
(
material.diffuse, TexCoords));
vec3
specular =
light.specular *
spec *
vec3
(
texture
(
material.specular, TexCoords));
return
(
ambient +
diffuse +
specular);
}
Nous avons simplement recopié le code du tutoriel précédent et utilisé les vecteurs passés en paramètres pour calculer la contribution de cette lumière directionnelle. Les composantes ambiante, diffuse et spéculaire sont ajoutées dans un unique vecteur couleur.
VI-B. Sources ponctuelles▲
Juste comme pour les sources directionnelles, nous définirons une fonction qui calcule la contribution d’une source ponctuelle sur un fragment, en tenant compte de l’atténuation. Nous définissons une structure qui comprend les données utiles pour ce type de source :
struct PointLight {
vec3
position;
float
constant;
float
linear;
float
quadratic;
vec3
ambient;
vec3
diffuse;
vec3
specular;
}
;
#
define
NR_POINT_LIGHTS
4
uniform
PointLight pointLights[NR_POINT_LIGHTS];
Comme on le voit, nous avons utilisé une directive du préprocesseur dans GLSL pour définir le nombre de sources ponctuelles de la scène. Nous utilisons ensuite la constante NR_POINT_LIGHTS pour créer un tableau de structures. Les tableaux en GLSL sont comme les tableaux en C et sont créés en utilisant deux crochets. Dès lors, nous avons quatre structures à initialiser.
On pourrait aussi définir une seule grande structure (au lieu d’une structure par type de source), contenant tous les champs nécessaires pour tous les types de sources et utiliser cette structure dans chaque fonction, en ignorant les champs non concernés. Cependant, je trouve l’approche proposée plus intuitive et plus efficace en termes de gestion mémoire, car une source ne requiert pas nécessairement tous les champs de tous les types de source.
Le prototype de la fonction pour une source ponctuelle est le suivant :
vec3
CalcPointLight
(
PointLight light, vec3
normal, vec3
fragPos, vec3
viewDir);
Cette fonction prend en paramètres les données nécessaires et retourne un vec3 qui représente la contribution de la source ponctuelle à la couleur finale du fragment. À nouveau, on utilise le code défini au chapitre précédent :
vec3
CalcPointLight
(
PointLight light, vec3
normal, vec3
fragPos, vec3
viewDir)
{
vec3
lightDir =
normalize
(
light.position -
fragPos);
// éclairage diffus
float
diff =
max
(
dot
(
normal, lightDir), 0
.0
);
// éclairage spéculaire
vec3
reflectDir =
reflect
(-
lightDir, normal);
float
spec =
pow
(
max
(
dot
(
viewDir, reflectDir), 0
.0
), material.shininess);
// atténuation
float
distance =
length
(
light.position – fragPos);
float
attenuation =
1
.0
/
(
light.constant +
light.linear *
distance +
light.quadratic *
(
distance *
distance));
// assemblage du résultat
vec3
ambient =
light.ambient *
vec3
(
texture
(
material.diffuse, TexCoords));
vec3
diffuse =
light.diffuse *
diff *
vec3
(
texture
(
material.diffuse, TexCoords));
vec3
specular =
light.specular *
spec *
vec3
(
texture
(
material.specular, TexCoords));
ambient *=
attenuation;
diffuse *=
attenuation;
specular *=
attenuation;
return
(
ambient +
diffuse +
specular);
}
Encapsuler cette fonctionnalité dans une fonction comme celle-ci a l’avantage de pouvoir calculer l’éclairage pour plusieurs sources ponctuelles sans avoir besoin de dupliquer le code pour chaque source. Dans la fonction main(), on utilisera une boucle qui appellera la fonction CalcPointLight() pour chacune des sources ponctuelles.
VI-C. Tout rassembler▲
Maintenant que nous disposons d’une fonction pour les sources directionnelles et une autre pour les sources ponctuelles, nous allons les appeler dans la fonction main() :
void
main
(
)
{
// Propriétés
vec3
norm =
normalize
(
Normal);
vec3
viewDir =
normalize
(
viewPos -
FragPos);
// phase 1 : éclairage directionnel
vec3
result =
CalcDirLight
(
dirLight, norm, viewDir);
// phase 2 : lumières ponctuelles
for
(
int
i =
0
; i <
NR_POINT_LIGHTS; i++
)
result +=
CalcPointLight
(
pointLights[i], norm, FragPos, viewDir);
// phase 3 : spot
//result += CalcSpotLight(spotLight, norm, FragPos, viewDir);
FragColor =
vec4
(
result, 1
.0
);
}
Chaque type de source apporte sa contribution à la couleur finale. Si vous le souhaitez, vous pouvez aussi définir une fonction pour les spots, ce que nous laissons en exercice pour le lecteur.
Initialiser les variables uniformes ne devrait pas paraître trop nouveau, mais vous pouvez vous demander comment le faire dans ce cas, car les variables uniformes pour les sources lumineuses sont dans un tableau de structures. Nous n’avons pas encore vu ce cas de figure.
Par chance, ce n’est pas si compliqué. Pour affecter une variable uniforme qui est un tableau de structures, on la traite comme le champ d’une structure, mais, en plus, il faut utiliser l’indice du tableau pour accéder à la structure :
lightingShader.setFloat("pointLights[0].constant"
, 1.0
f);
Ici nous pointons la première structure du tableau pointLights et le champ constant de cette structure. Cela implique qu’il faudra affecter manuellement tous les champs de chacune des quatre sources ponctuelles, soit 28 appels, ce qui est fastidieux. On pourrait essayer d’améliorer cela en utilisant une classe pour les sources ponctuelles, mais il faudrait toujours initialiser ces variables uniformes.
N’oublions pas que nous devons aussi définir un vecteur position pour chaque source ponctuelle, que nous répartissons dans la scène. Ces positions seront définies par un tableau de vecteurs vec3 :
glm::
vec3 pointLightPositions[] =
{
glm::
vec3( 0.7
f, 0.2
f, 2.0
f),
glm::
vec3( 2.3
f, -
3.3
f, -
4.0
f),
glm::
vec3(-
4.0
f, 2.0
f, -
12.0
f),
glm::
vec3( 0.0
f, 0.0
f, -
3.0
f)
}
;
Pointez ensuite la structure du tableau pointLights correspondant à la source et affectez sa position avec les valeurs du tableau de positions. Pensez à afficher quatre cubes pour les lampes, en créant une matrice de modèle pour chacun d’entre eux, comme nous l’avons fait pour les conteneurs.
Si vous utilisez aussi une lampe frontale, vous devriez obtenir une image comme celle-ci :
On distingue une sorte de lumière globale (comme le soleil) quelque part dans le ciel, nous avons quatre lampes réparties dans la scène et une lampe frontale est visible pour le spectateur.
Vous trouverez le code de l’application ici.
L’image est obtenue avec toutes les sources de lumière paramétrées avec les propriétés définies dans les tutoriels précédents, mais si vous modifiez ces valeurs vous pourrez obtenir des résultats intéressants. Les artistes et les éditeurs de niveaux règlent ces valeurs dans un éditeur pour que les éclairages soient conformes à leurs souhaits. En utilisant le simple environnement éclairé que nous venons de créer, vous pouvez obtenir des résultats visuels intéressants en jouant sur les paramètres des sources lumineuses :
Nous avons aussi modifié la couleur du fond pour mieux correspondre à l’éclairage. On voit qu’en jouant sur la lumière, on obtient des atmosphères très variées.
Maintenant, vous devriez avoir une assez bonne compréhension de l’éclairage dans OpenGL. Avec ces connaissances, vous pouvez déjà créer des environnements et des atmosphères visuellement avancées.
VI-D. Exercices▲
- Pouvez-vous recréer de nouvelles atmosphères de la dernière scène en modifiant les attributs des sources lumineuses ? Solution.
VI-E. Remerciements▲
Ce tutoriel est une traduction réalisée par Jean-Michel Fray dont l’original a été écrit par Joey de Vries et qui est disponible sur le site Learn OpenGL.