
La leçon
précédente a donné un aperçu des bases de la mise en œuvre d'un modèle de rendu physiquement plausible. Cette fois, nous passerons des calculs théoriques à une implémentation de rendu spécifique avec la participation de sources lumineuses directes (analytiques): de type point, directionnel ou spot.
Tout d'abord, rafraîchissons l'expression pour calculer la réflectivité de la leçon précédente:
Pour la plupart, nous avons déjà traité des composants de cette formule, mais la question reste de savoir comment représenter spécifiquement l'
irradiance , qui est la luminosité énergétique totale (
radiance )
toute la scène. Nous avons convenu que la luminosité énergétique
(en termes de terminologie informatique) est considéré comme le rapport du flux de rayonnement (
flux rayonnant )
(énergie de rayonnement de la source lumineuse) à la valeur de l'angle solide
. Dans notre cas, l'angle solide
nous l'avons pris comme infinitésimal, et donc la luminosité énergétique donne une idée du flux de rayonnement pour chaque rayon de lumière individuel (sa direction).
Comment lier ces calculs au modèle d'éclairage que nous connaissons des leçons précédentes? Tout d'abord, imaginez que vous disposez d'une source de lumière ponctuelle unique (qui émet uniformément dans toutes les directions) avec un flux de rayonnement défini comme une triade RVB (23,47, 21,31, 20,79). L'
intensité radiante
d' une telle source est égale à son flux de rayonnement dans toutes les directions. Cependant, après avoir examiné le problème de la détermination de la couleur d'un point spécifique
en surface, vous pouvez voir celle de toutes les directions possibles d'incidence de la lumière dans l'hémisphère
seul vecteur
proviendra évidemment d'une source lumineuse. Puisqu'une seule source de lumière est représentée, représentée par un point dans l'espace, pour toutes les autres directions possibles d'incidence de la lumière dans un point
la luminosité énergétique sera égale à zéro:
Maintenant, si nous ne prenons pas temporairement en compte la loi d'amortissement de la lumière pour une source donnée, il s'avère que la luminosité énergétique du faisceau lumineux incident de cette source reste inchangée partout où nous plaçons la source (échelle de luminosité basée sur le cosinus de l'angle d'incidence
ne compte pas non plus). Au total, une source ponctuelle maintient la force de rayonnement constante quel que soit l'angle de vue, ce qui équivaut à prendre la force de rayonnement égale au flux de rayonnement initial sous la forme d'une constante de triade (23,47, 21,31, 20,79).
Cependant, le calcul de la luminosité énergétique est également basé sur la coordonnée du point
, au moins n'importe quelle source de lumière physiquement fiable montre l'atténuation de la force de rayonnement avec une distance croissante d'un point à une source. Vous devez également prendre en compte l'orientation de la surface, comme le montre l'expression originale pour la luminosité: le résultat du calcul de la force de rayonnement doit être multiplié par la valeur scalaire du vecteur normal à la surface
et vecteur d'incidence de rayonnement
.
Pour réécrire ce qui précède: pour une source de lumière ponctuelle directe, la fonction de rayonnement
détermine la couleur de la lumière incidente, en tenant compte de l'atténuation à une distance donnée du point
et en tenant compte de la mise à l'échelle par un facteur
mais seulement pour un seul rayon de lumière
aller droit au but
- essentiellement le seul vecteur reliant la source et le point. Sous forme de code source, cela est interprété comme suit:
vec3 lightColor = vec3(23.47, 21.31, 20.79); vec3 wi = normalize(lightPos - fragPos); float cosTheta = max(dot(N, Wi), 0.0); float attenuation = calculateAttenuation(fragPos, lightPos); vec3 radiance = lightColor * attenuation * cosTheta;
Si vous fermez les yeux sur une terminologie légèrement modifiée, ce morceau de code devrait vous rappeler quelque chose. Oui, oui, c'est le même code pour calculer la composante diffuse dans le modèle d'éclairage que nous connaissons. Pour un éclairage direct, la luminosité de l'énergie est déterminée par un seul vecteur pour la source de lumière, car le calcul est effectué d'une manière si similaire à celle que nous connaissons encore.
Je note que cette affirmation n'est vraie que sous l'hypothèse qu'une source ponctuelle de lumière est infinitésimale et est représentée par un point dans l'espace. Lors de la modélisation d'une source de volume, sa luminosité sera différente de zéro dans de nombreuses directions, et pas seulement sur un faisceau.
Pour d'autres sources lumineuses émettant un rayonnement à partir d'un seul point, la luminosité énergétique est calculée de la même manière. Par exemple, une source de lumière directionnelle a une direction constante
et n'utilise pas d'atténuation, et la source de projection montre une puissance de rayonnement variable, selon la direction de la source.
Nous revenons ici à la valeur de l'intégrale
à la surface de l'hémisphère
. Puisque nous connaissons à l'avance les positions de toutes les sources de lumière participant à l'ombrage d'un point particulier, nous n'avons pas besoin d'essayer de résoudre l'intégrale. Nous pouvons calculer directement l'irradiation totale fournie par ce nombre de sources lumineuses, car la luminosité énergétique de la surface est affectée par une seule direction pour chaque source.
Par conséquent, le calcul PBR pour les sources de lumière directe est une question assez simple, car tout se résume à une recherche séquentielle des sources impliquées dans l'éclairage. Plus tard, un composant de l'environnement apparaîtra dans le modèle d'éclairage, sur lequel nous travaillerons dans le tutoriel sur l'éclairage basé sur l'image (
Image-Based Lighting ,
IBL ). Il n'y a pas d'échappatoire à l'estimation de l'intégrale, car la lumière dans un tel modèle tombe de plusieurs directions.
Modèle de surface PBR
Commençons par le fragment shader qui implémente le modèle PBR décrit ci-dessus. Tout d'abord, nous définissons les données d'entrée nécessaires pour l'ombrage de surface:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; uniform vec3 camPos; uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao;
Ici, vous pouvez voir l'entrée habituelle calculée à l'aide du vertex shader le plus simple, ainsi qu'un ensemble d'uniformes qui décrivent les caractéristiques de surface de l'objet.
De plus, au tout début du code du shader, nous effectuons des calculs qui sont si familiers de la mise en œuvre du modèle d'éclairage Blinn-Fong:
void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); [...] }
Éclairage direct
L'exemple de cette leçon ne contient que quatre sources lumineuses ponctuelles qui spécifient clairement l'irradiation de la scène. Pour satisfaire l'expression de la réflectivité, nous parcourons itérativement chaque source de lumière, calculons la luminosité énergétique individuelle et résumons cette contribution, en modulant simultanément la valeur BRDF et l'angle d'incidence du faisceau lumineux. Vous pouvez imaginer cette itération comme une solution de l'intégrale sur la surface
Pour les sources lumineuses analytiques uniquement.
Donc, nous calculons d'abord les valeurs calculées pour chaque source:
vec3 Lo = vec3(0.0); for(int i = 0; i < 4; ++i) { vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); float attenuation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; [...]
Comme les calculs sont effectués dans un espace linéaire (
une correction gamma est effectuée à la fin du shader), une loi d'atténuation plus correcte physiquement est utilisée en fonction du carré inverse de la distance:
Supposons que la loi du carré inverse soit plus correcte physiquement, afin de mieux contrôler la nature de l'amortissement, il est tout à fait possible d'utiliser la formule déjà familière contenant des termes constants, linéaires et quadratiques.
De plus, pour chaque source, nous calculons également la valeur du miroir Cook-Torrance BRDF:
La première étape consiste à calculer le rapport entre la réflexion spéculaire et diffuse, ou, en d'autres termes, le rapport entre la quantité de lumière réfléchie et la quantité de lumière réfractée par la surface. De la
leçon précédente, nous savons à quoi ressemble le calcul du coefficient de Fresnel:
vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }
L'approximation de Fresnel-Schlick attend le paramètre
F0 à l'entrée, qui montre le
degré de réflexion de surface à un angle d'incidence de lumière nul , c'est-à-dire degré de réflexion, si vous regardez la surface le long de la normale de haut en bas. La valeur de
F0 varie en fonction du matériau et acquiert une dominante de couleur pour les métaux, comme on peut le voir en consultant les catalogues de matériaux PBR. Pour le processus de
workflow métallique (processus de création de matériaux PBR, division de tous les matériaux en classes de diélectriques et de conducteurs), il est supposé que tous les diélectriques semblent assez fiables à une valeur constante de
F0 = 0,04 , tandis que pour les surfaces métalliques,
F0 est défini en fonction de l'albédo de surface. Sous forme de code:
vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
Comme vous pouvez le voir, pour les surfaces strictement non métalliques,
F0 est fixé à 0,04. Mais en même temps, il peut facilement passer de cette valeur à la valeur d'albédo sur la base de la «métallicité» de la surface. Cet indicateur est généralement présenté comme une texture distincte (à partir d'ici, en fait,
le flux de travail
métallique est pris,
environ Trans. ).
Ayant reçu
nous devons calculer la valeur de la fonction de distribution normale
et fonctions de géométrie
:
Code de fonction du boîtier avec éclairage analytique:
float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; float NdotH = max(dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float num = a2; float denom = (NdotH2 * (a2 - 1.0) + 1.0); denom = PI * denom * denom; return num / denom; } float GeometrySchlickGGX(float NdotV, float roughness) { float r = (roughness + 1.0); float k = (r*r) / 8.0; float num = NdotV; float denom = NdotV * (1.0 - k) + k; return num / denom; } float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) { float NdotV = max(dot(N, V), 0.0); float NdotL = max(dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; }
Une différence importante par rapport à celle décrite dans la
partie théorique : ici on passe directement le paramètre de rugosité à toutes les fonctions mentionnées. Ceci est fait pour permettre à chaque fonction de modifier la valeur de rugosité d'origine à sa manière. Par exemple, des études Disney, reflétées dans le moteur d'Epic Games, ont montré que le modèle d'éclairage donne des résultats visuellement plus corrects si nous utilisons le carré de la rugosité dans la fonction géométrique et la fonction de distribution normale.
Après avoir réglé toutes les fonctions, on peut directement obtenir les valeurs NDF et G:
float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness);
Au total, nous avons à portée de main toutes les valeurs pour le calcul de la totalité du BRDF Cook-Torrance:
vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001; vec3 specular = numerator / denominator;
Veuillez noter que nous limitons le dénominateur à une valeur minimale de 0,001 pour éviter la division par zéro en cas de mise à zéro du produit scalaire.
Nous allons maintenant calculer la contribution de chaque source à l'équation de réflectivité. Le coefficient de Fresnel étant directement une variable
, alors nous pouvons utiliser la valeur de F pour indiquer la contribution de la source à la réflexion spéculaire de la surface. De la quantité
peut être obtenu et l'indice de réfraction
:
vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic;
Puisque nous considérons la quantité
kS représentant la quantité d'énergie lumineuse comme une surface réfléchie, en la soustrayant de l'unité, nous obtenons l'énergie résiduelle de la lumière
kD réfractée par la surface. De plus, comme les métaux ne réfractent pas la lumière et n'ont pas de composante diffuse de lumière réémise, la composante
kD sera modulée pour être nulle pour un matériau tout métal. Après ces calculs, nous disposerons de toutes les données pour calculer la réflectance fournie par chacune des sources lumineuses:
const float PI = 3.14159265359; float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; }
La valeur finale
Lo , ou luminosité de l'énergie sortante, est essentiellement une solution à l'expression de la réflectivité, c'est-à-dire résultat d'intégration de surface
. Dans ce cas, nous n'avons pas besoin d'essayer de résoudre l'intégrale sous une forme générale pour toutes les directions possibles, car dans cet exemple, il n'y a que quatre sources de lumière qui affectent le fragment en cours de traitement. C'est pourquoi toute «intégration» se limite à un simple cycle de sources lumineuses existantes.
Il ne reste plus qu'à ajouter la similitude du composant d'éclairage de fond aux résultats du calcul de la source de lumière directe et la couleur finale du fragment est prête:
vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo;
Rendu linéaire et HDR
Jusqu'à présent, nous avons supposé que tous les calculs sont effectués dans un espace colorimétrique linéaire, et avons donc utilisé
la correction gamma comme accord final dans notre shader. La réalisation de calculs dans un espace linéaire est extrêmement importante pour la simulation correcte de PBR, car le modèle nécessite la linéarité de toutes les données d'entrée. Essayez de ne pas garantir la linéarité de l'un des paramètres et le résultat de l'ombrage sera incorrect. De plus, il serait intéressant de régler les sources lumineuses avec des caractéristiques proches des sources réelles: par exemple, la couleur de leur rayonnement et la luminosité énergétique peuvent varier librement sur une large plage. Par conséquent,
Lo peut facilement accepter de grandes valeurs, mais tombe inévitablement sous la coupure dans l'intervalle [0., 1.] en raison de la faible plage dynamique (
LDR ) du tampon de trame par défaut.
Pour éviter la perte des valeurs HDR, avant la correction gamma, il est nécessaire d'effectuer une compression de tonalité:
color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2));
L'opérateur familier de Reinhardt est utilisé ici, ce qui nous permet de maintenir une large plage dynamique dans des conditions d'irradiation très changeante de différentes parties de l'image. Comme nous n'utilisons pas ici un shader séparé pour le post-traitement, les opérations décrites peuvent être ajoutées simplement à la fin du code du shader.
Je répète que pour la modélisation correcte de PBR, il est extrêmement important de se souvenir et de considérer les caractéristiques du travail avec l'espace colorimétrique linéaire et le rendu HDR. La négligence de ces aspects entraînera des calculs incorrects et des résultats visuellement inesthétiques.
Shader PBR pour éclairage analytique
Ainsi, avec les touches finales sous forme de compression tonale et de correction gamma, il ne reste plus qu'à transférer la couleur finale du fragment à la sortie du shader de fragment et le code de shader PBR pour l'éclairage direct peut être considéré comme terminé. Enfin, regardons tout le code de la fonction
main () de ce shader:
#version 330 core out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal;
J'espère qu'après avoir lu la
partie théorique et avec l'analyse d'aujourd'hui de l'expression de la capacité de réflexion, cette liste cessera d'être intimidante.
Nous utilisons ce shader dans une scène contenant quatre sources lumineuses ponctuelles, un certain nombre de sphères dont les caractéristiques de surface vont changer le degré de leur rugosité et de leur métallicité le long des axes horizontal et vertical, respectivement. À la sortie, nous obtenons l'image suivante:
La métallicité passe de zéro à un de bas en haut, et la rugosité est similaire, mais de gauche à droite. Il devient évident qu'en changeant uniquement ces deux caractéristiques de surface, il est déjà possible de définir une large gamme de matériaux.
Le code source complet est
ici .
PBR et texturation
Nous élargirons notre modèle de surface en transmettant des caractéristiques sous forme de textures. De cette façon, nous pouvons fournir un contrôle par fragment des paramètres du matériau de surface:
[...] uniform sampler2D albedoMap; uniform sampler2D normalMap; uniform sampler2D metallicMap; uniform sampler2D roughnessMap; uniform sampler2D aoMap; void main() { vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, 2.2); vec3 normal = getNormalFromNormalMap(); float metallic = texture(metallicMap, TexCoords).r; float roughness = texture(roughnessMap, TexCoords).r; float ao = texture(aoMap, TexCoords).r; [...] }
Notez que la texture de l'albédo de surface est généralement créée par des artistes dans l'espace colorimétrique sRGB.Par conséquent, dans le code ci-dessus, nous renvoyons la couleur texel à l'espace linéaire afin qu'elle puisse être utilisée dans d'autres calculs. Selon la façon dont les artistes créent la texture contenant les données de la
carte d'occlusion ambiante , il peut également être nécessaire de la placer dans un espace linéaire. Les cartes de métallicité et de rugosité sont presque toujours créées dans un espace linéaire.
L'utilisation de textures au lieu de paramètres de surface fixes en combinaison avec l'algorithme PBR donne une augmentation significative de la fiabilité visuelle par rapport aux algorithmes d'éclairage précédemment utilisés:
L'exemple de code de texturation complet est
ici , et les textures utilisées sont
ici (avec la texture d'ombrage d'arrière-plan). J'attire votre attention sur le fait que les surfaces fortement métalliques apparaissent assombries dans des conditions d'éclairage direct, car la contribution de la réflexion diffuse est faible (à la limite il n'y en a pas du tout). Leur ombrage ne devient plus correct qu'en prenant en compte la réflexion miroir de l'éclairage de l'environnement, ce que nous ferons dans les prochaines leçons.
Pour le moment, le résultat n'est peut-être pas aussi impressionnant que certaines démonstrations PBR - pourtant, nous n'avons pas encore implémenté un système d'éclairage basé sur l'image (
IBL ). Néanmoins, notre rendu est désormais considéré comme basé sur des principes physiques et, même sans IBL, il montre une image plus fiable qu'auparavant.