Apprenez OpenGL. Leçon 6.2 - Rendu basé sur la physique. Sources de lumière analytiques

OGL3 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:

Lo(p, omegao)= int limits Omega(kd fracc pi+ fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai


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 ) Ltoute la scène. Nous avons convenu que la luminosité énergétique L(en termes de terminologie informatique) est considéré comme le rapport du flux de rayonnement ( flux rayonnant )  phi(énergie de rayonnement de la source lumineuse) à la valeur de l'angle solide  omega. Dans notre cas, l'angle solide  omeganous 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 pen surface, vous pouvez voir celle de toutes les directions possibles d'incidence de la lumière dans l'hémisphère  Omegaseul vecteur wiproviendra é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 pla 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  phine 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 p, 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 net vecteur d'incidence de rayonnement wi.

Pour réécrire ce qui précède: pour une source de lumière ponctuelle directe, la fonction de rayonnement Ldétermine la couleur de la lumière incidente, en tenant compte de l'atténuation à une distance donnée du point pet en tenant compte de la mise à l'échelle par un facteur n cdotwimais seulement pour un seul rayon de lumière wialler droit au but p- 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 wiet 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  intà la surface de l'hémisphère  Omega. 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  OmegaPour 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:

 fracDFG4( omegao cdotn)( omegai cdotn)


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 Fnous devons calculer la valeur de la fonction de distribution normale Det fonctions de géométrie G:

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 Ks, 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é Kspeut être obtenu et l'indice de réfraction Kd:

 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  Omega. 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; //   uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; //   uniform vec3 lightPositions[4]; uniform vec3 lightColors[4]; uniform vec3 camPos; const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness); float GeometrySchlickGGX(float NdotV, float roughness); float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness); vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness); void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); vec3 F0 = vec3(0.04); F0 = mix(F0, albedo, metallic); //    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; // Cook-Torrance BRDF float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0); vec3 kS = F; vec3 kD = vec3(1.0) - kS; kD *= 1.0 - metallic; vec3 numerator = NDF * G * F; float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); vec3 specular = numerator / max(denominator, 0.001); //       Lo float NdotL = max(dot(N, L), 0.0); Lo += (kD * albedo / PI + specular) * radiance * NdotL; } vec3 ambient = vec3(0.03) * albedo * ao; vec3 color = ambient + Lo; color = color / (color + vec3(1.0)); color = pow(color, vec3(1.0/2.2)); FragColor = vec4(color, 1.0); } 

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.

Source: https://habr.com/ru/post/fr424453/


All Articles