IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Apprendre OpenGL moderne

Deuxième partie : éclairage


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

c17_1_1
Sélectionnez
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 :

c17_1_2
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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() :

 
Sélectionnez
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 :

 
Sélectionnez
lightingShader.setFloat("pointLights[0].constant", 1.0f);

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 :

 
Sélectionnez
glm::vec3 pointLightPositions[] = {
        glm::vec3( 0.7f,  0.2f,  2.0f),
        glm::vec3( 2.3f, -3.3f, -4.0f),
        glm::vec3(-4.0f,  2.0f, -12.0f),
        glm::vec3( 0.0f,  0.0f, -3.0f)
};

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 :

Image non disponible

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 :

Image non disponible

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.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2018 Joey de Vries. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.