
L'éclairage basé sur l'image ou
IBL (
Image Based Lighting ) est une catégorie de méthodes d'éclairage basées non pas sur la prise en compte des sources de lumière analytiques (discutées dans la
leçon précédente ), mais en considérant l'environnement entier des objets illuminés comme une seule source de lumière continue. Dans le cas général, la base technique de telles méthodes réside dans le traitement d'une carte cubique de l'environnement (préparée dans le monde réel ou créée à partir d'une scène en trois dimensions) afin que les données stockées dans la carte puissent être directement utilisées dans les calculs d'éclairage: en fait, chaque texel de la carte cubique est considéré comme une source lumineuse . En général, cela vous permet de capturer l'effet de l'éclairage global dans la scène, qui est un composant important qui transmet le «ton» global de la scène actuelle et aide les objets illuminés à mieux y être «intégrés».
Étant donné que les algorithmes IBL prennent en compte l'éclairage provenant d'un certain environnement «global», leur résultat est considéré comme une simulation plus précise de l'éclairage de fond ou même une approximation très approximative de l'éclairage global. Cet aspect rend les méthodes IBL intéressantes en termes d'incorporation du PBR dans le modèle, car l'utilisation de la lumière ambiante dans le modèle d'éclairage permet aux objets d'avoir une apparence beaucoup plus correcte physiquement.
Pour incorporer l'influence d'IBL dans le système PBR déjà décrit, nous revenons à l'équation de réflectance familière:
Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn)))Li(p, omegai)n cdot omegaid omegai
Comme décrit précédemment, l'objectif principal est de calculer l'intégrale pour toutes les directions de rayonnement entrant
wi hémisphère
Omega . Dans la
dernière leçon, le calcul de l'intégrale n'était pas fastidieux, car nous connaissions à l'avance le nombre de sources lumineuses, et donc toutes ces différentes directions d'incidence lumineuse qui leur correspondent. En même temps, l'intégrale ne peut pas être résolue avec un accrochage:
tout vecteur tombant
wi de l'environnement peut transporter une luminosité énergétique non nulle. En conséquence, pour l'applicabilité pratique de la méthode, elle doit satisfaire aux exigences suivantes:
- Vous devez trouver un moyen d'obtenir la luminosité énergétique de la scène pour un vecteur de direction arbitraire wi ;
- Il est nécessaire que la solution de l'intégrale puisse se produire en temps réel.
Eh bien, le premier point est résolu par lui-même. Un indice d'une solution a déjà glissé ici: l'une des méthodes pour représenter l'irradiation d'une scène ou d'un environnement est une carte cubique qui a subi un traitement spécial. Chaque texel d'une telle carte peut être considéré comme une source d'émission distincte. En échantillonnant à partir d'une telle carte selon un vecteur arbitraire
wi on obtient facilement la luminosité énergétique de la scène dans cette direction.
Donc, nous obtenons la luminosité énergétique de la scène pour un vecteur arbitraire
wi :
vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
Remarquablement, cependant, la résolution de l'intégrale nous oblige à faire des échantillons de la carte de l'environnement non pas dans une direction, mais à partir de tous les possibles dans l'hémisphère. Et ainsi - pour chaque fragment ombré. De toute évidence, pour les tâches en temps réel, cela est pratiquement impossible. Une méthode plus efficace serait de calculer à l'avance une partie des opérations de l'intégrande, même en dehors de notre application. Mais pour cela, vous devrez retrousser vos manches et plonger plus profondément dans l'essence de l'expression de la réflectivité:
Lo(p, omegao)= int limits Omega(kd fracc pi+ks fracDFG4( omegao cdotn)( omegai cdotn)))Li(p, omegai)n cdot omegaid omegai
On peut voir que les parties de l'expression liées au diffus
kd et miroir
ks Les composants BRDF sont indépendants. Vous pouvez diviser l'intégrale en deux parties:
Lo(p, omegao)= int limits Omega(kd fracc pi)Li(p, omegai)n cdot omegaid omegai+ int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn))Li(p, omegai)n cdot omegaid omegai
Une telle division en parties nous permettra de traiter chacune d'elles individuellement, et dans cette leçon nous traiterons de la partie responsable de l'éclairage diffus.
Après avoir analysé la forme de l'intégrale sur la composante diffuse, nous pouvons conclure que la composante diffuse de Lambert est essentiellement constante (couleur
s indice de réfraction
kd et
pi sont constants dans les conditions d'intégrande) et ne dépend pas d'autres variables. Compte tenu de ce fait, nous pouvons mettre les constantes au-delà du signe de l'intégrale:
Lo(p, omegao)=kd fracc pi int limits OmegaLi(p, omegai)n cdot omegaid omegai
Nous obtenons donc une intégrale ne dépendant que de
wi (on suppose que
p correspond au centre de la carte cubique de l'environnement). Sur la base de cette formule, vous pouvez calculer ou, mieux encore, pré-calculer une nouvelle carte cubique qui stocke le résultat du calcul de l'intégrale de la composante diffuse pour chaque direction de l'échantillon (ou carte texel)
wo en utilisant l'opération de convolution.
La convolution consiste à appliquer un calcul à chaque élément d'un ensemble de données, en tenant compte des données de tous les autres éléments de l'ensemble. Dans ce cas, ces données correspondent à la luminosité énergétique de la scène ou de la carte de l'environnement. Ainsi, pour calculer une valeur dans chaque direction de l'échantillon dans la carte cubique, nous devrons prendre en compte les valeurs prises dans toutes les autres directions possibles de l'échantillon dans l'hémisphère situées autour du point d'échantillonnage.
Pour convoluer la carte d'environnement, vous devez résoudre l'intégrale pour chaque direction résultante de l'échantillon
wo en effectuant plusieurs échantillons discrets le long des directions
wi appartenant à l'hémisphère
Omega et la moyenne de la luminosité énergétique totale. L'hémisphère, sur la base duquel les directions d'échantillonnage sont prises
wi orienté le long du vecteur
wo représentant la direction de destination pour laquelle la convolution actuelle est calculée. Regardez l'image pour une meilleure compréhension:
Une telle carte cubique pré-calculée qui stocke le résultat de l'intégration pour chaque direction de l'échantillon
wo peut également être considéré comme stockant le résultat de la somme de tous les éclairages diffus indirects de la scène, incident sur une certaine surface orientée le long de la direction
wo . En d'autres termes, ces cartes cubiques sont appelées cartes d'irradiance, car la carte d'environnement cubique pré-convolutionnelle vous permet d'échantillonner directement l'ampleur de l'irradiation de la scène, provenant d'une direction arbitraire
wo , sans calculs supplémentaires.
L'expression déterminant la luminosité énergétique dépend également de la position du point d'échantillonnage p que nous avons pris se trouvant en plein centre de la carte d'irradiation. Cette hypothèse impose une limitation dans le sens où la source de tout éclairage diffus indirect sera également une carte environnementale unique. Dans les scènes hétérogènes en éclairage, cela peut détruire l'illusion de la réalité (en particulier dans les scènes d'intérieur). Les moteurs de rendu modernes résolvent ce problème en plaçant des objets auxiliaires spéciaux dans les sondes de réflexion de scène. Chacun de ces objets est engagé dans une tâche: il forme sa propre carte d'irradiation pour son environnement immédiat. Avec cette technique, l'irradiation (et la luminosité énergétique) à un point arbitraire p sera déterminé par simple interpolation entre les échantillons de réflexion les plus proches. Mais pour les tâches actuelles, nous convenons que la carte de l'environnement est échantillonnée à partir de son centre, et nous analyserons des échantillons de réflexion dans d'autres leçons.
Vous trouverez ci-dessous un exemple de carte cubique de l'environnement et une carte d'irradiation (basée sur le
moteur à vagues ) dérivée de celui-ci, qui fait la moyenne de la luminosité énergétique de l'environnement pour chaque direction de sortie
wo .
Ainsi, cette carte stocke le résultat de convolution dans chaque texel (correspondant à la direction
wo ), et extérieurement, une telle carte ressemble à l'enregistrement de la couleur moyenne de la carte d'environnement. Un échantillon dans n'importe quelle direction d'une telle carte retournera la valeur de l'irradiation émanant de cette direction.
PBR et HDR
Dans la
leçon précédente , il avait déjà été brièvement noté que pour le bon fonctionnement du modèle d'éclairage PBR, il était extrêmement important de prendre en compte la plage de luminosité HDR des sources lumineuses présentes. Étant donné que le modèle PBR à l'entrée accepte les paramètres d'une manière ou d'une autre sur la base de quantités et de caractéristiques physiques très spécifiques, il est logique d'exiger que la luminosité énergétique des sources lumineuses corresponde à leurs prototypes réels. Peu importe la façon dont nous justifions la valeur spécifique du flux de rayonnement pour chaque source: nous faisons une estimation technique approximative ou nous tournons vers
des quantités physiques - la différence de caractéristiques entre une lampe d'ambiance et le soleil sera de toute façon énorme. Sans l'utilisation de la gamme
HDR , il sera tout simplement impossible de déterminer avec précision la luminosité relative d'une variété de sources de lumière.
Donc, PBR et HDR sont amis pour toujours, c'est compréhensible, mais comment ce fait est-il lié aux méthodes d'éclairage basées sur l'image? Dans la dernière leçon, il a été démontré que la conversion de PBR en plage de rendu HDR est facile. Il reste un «mais»: comme l'éclairage indirect de l'environnement est basé sur une carte cubique de l'environnement, un moyen est nécessaire pour préserver les caractéristiques HDR de cet éclairage de fond dans la carte de l'environnement.
Jusqu'à présent, nous avons utilisé des cartes d'environnement créées au format LDR (comme les
skyboxes ). Nous avons utilisé l'échantillon de couleur d'eux dans le rendu tel quel, ce qui est tout à fait acceptable pour l'ombrage direct des objets. Et il est totalement inadapté lors de l'utilisation de cartes d'environnement comme sources de mesures physiquement fiables.
RGBE - Format d'image HDR
Familiarisez-vous avec le format de fichier image RGBE. Les fichiers avec l'extension "
.hdr " sont utilisés pour stocker des images avec une large plage dynamique, allouant un octet pour chaque élément de la triade de couleurs et un octet de plus pour l'exposant commun. Le format vous permet également de stocker des cartes d'environnement cubique avec une plage d'intensité de couleur au-delà de la plage LDR [0., 1.]. Cela signifie que les sources lumineuses peuvent maintenir leur intensité réelle, étant représentées par une telle carte de l'environnement.
Le réseau possède un grand nombre de cartes d'environnement gratuites au format RGBE, tournées dans diverses conditions réelles. Voici un exemple du site d'
archives sIBL :
Vous pouvez être surpris de ce que vous avez vu: après tout, cette image déformée ne ressemble pas du tout à une carte cubique régulière avec sa décomposition prononcée en 6 faces. L'explication est simple: cette carte de l'environnement a été projetée d'une sphère sur un plan - un
scan rectangulaire égal a été appliqué. Ceci est fait pour pouvoir stocker dans un format qui ne prend pas en charge le mode de stockage des cartes cubiques tel quel. Bien sûr, cette méthode de projection a ses inconvénients: la résolution horizontale est beaucoup plus élevée que la verticale. Dans la plupart des cas d'application dans le rendu, il s'agit d'un rapport acceptable, car généralement les détails intéressants de l'environnement et de l'éclairage sont situés exactement dans le plan horizontal et non dans le plan vertical. Eh bien, en plus de tout, nous avons besoin du code de conversion pour revenir à la carte cubique.
Prise en charge du format RGBE dans stb_image.h
Le téléchargement de ce format d'image par vous-même nécessite une connaissance des
spécifications de format , ce qui n'est pas difficile, mais toujours laborieux. Heureusement pour
nous , la bibliothèque de chargement d'image
stb_image.h , implémentée dans un seul fichier d'en-tête, prend en charge le chargement de fichiers RGBE, renvoyant un tableau de nombres à virgule flottante - ce dont nous avons besoin pour nos besoins! Ajouter une bibliothèque à votre projet, charger des données d'image est extrêmement simple:
#include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; }
La bibliothèque convertit automatiquement les valeurs du format HDR interne en nombres réels réels de 32 bits, avec trois canaux de couleur par défaut. Il suffit d'enregistrer les données de l'image HDR d'origine dans une texture à virgule flottante 2D normale.
Convertir un balayage à angle égal en une carte cubique
Un balayage également rectangulaire peut être utilisé pour sélectionner directement des échantillons de la carte de l'environnement, cependant, cela nécessiterait des opérations mathématiques coûteuses, tandis que la récupération à partir d'une carte cubique normale serait pratiquement gratuite. C'est précisément à partir de ces considérations que dans cette leçon nous traiterons de la conversion d'une image également rectangulaire en une carte cubique, qui sera utilisée plus tard. Cependant, la méthode d'échantillonnage direct à partir d'une carte également rectangulaire utilisant un vecteur tridimensionnel sera également présentée ici, afin que vous puissiez choisir la méthode de travail qui vous convient.
Pour convertir, vous devez dessiner un cube de taille unitaire, en l'observant de l'intérieur, projeter une carte rectangulaire égale sur ses faces, puis extraire six images des faces en tant que faces de la carte cubique. Le vertex shader de cette étape est assez simple: il traite simplement les sommets du cube tel quel et transmet également leurs positions non réformées au fragment shader pour l'utiliser comme vecteur d'échantillon en trois dimensions:
#version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); }
Dans le shader de fragment, nous ombrons chaque face du cube comme si nous essayions d'envelopper doucement le cube avec une feuille avec une carte également rectangulaire. Pour ce faire, la direction de l'échantillon transférée au fragment shader est prise, traitée par une magie trigonométrique spéciale, et, finalement, la sélection est effectuée à partir d'une carte rectangulaire égale comme s'il s'agissait en fait d'une carte cubique. Le résultat de la sélection est directement enregistré en tant que couleur du fragment de la face du cube:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); }
Si vous dessinez un cube avec ce shader et une carte d'environnement HDR associée, vous obtenez quelque chose comme ceci:
C'est-à-dire on peut voir qu'en fait nous avons projeté une texture rectangulaire sur un cube. Très bien, mais comment cela nous aidera-t-il à créer une vraie carte cubique? Pour terminer cette tâche, il est nécessaire de rendre le même cube 6 fois avec une caméra regardant chacune des faces, tout en écrivant la sortie dans un objet
tampon d'image séparé:
unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
Bien sûr, nous n'oublierons pas d'organiser la mémoire pour stocker chacune des six faces de la future carte cubique:
unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) {
Après cette préparation, il ne reste plus qu'à effectuer directement le transfert de parties d'une carte rectangulaire égale au bord d'une carte cubique.
Nous n'entrerons pas dans trop de détails, d'autant plus que le code se répète beaucoup vu dans les leçons sur le
tampon de trame et les
ombres omnidirectionnelles . En principe, tout se résume à préparer six matrices de vue distinctes orientant strictement la caméra vers chacune des faces du cube, ainsi qu'une matrice de projection spéciale avec un angle de vue de 90 ° pour capturer toute la face du cube. Ensuite, seulement six fois, le rendu est effectué et le résultat est enregistré dans un tampon d'images à virgule flottante:
glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); glm::mat4 captureViews[] = { glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) };
Ici, la couleur du tampon d'image est attachée et change alternativement la face connectée de la carte cubique, ce qui conduit à la sortie directe du rendu sur l'une des faces de la carte d'environnement. Ce code doit être exécuté une seule fois, après quoi nous aurons
toujours une carte d'environnement
envCubemap à part entière contenant le résultat de la conversion de la version rectangulaire rectangulaire d'origine de la carte d'environnement HDR.
Nous allons tester la carte cubique résultante en esquissant le shader skybox le plus simple:
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; // mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; }
Faites attention à l'astuce avec les composants du vecteur
clipPos : nous utilisons la tétrade
xyww lors de l'enregistrement des coordonnées transformées du sommet pour nous assurer que tous les fragments de la skybox ont une profondeur maximale de 1,0 (l'approche a déjà été utilisée dans la
leçon correspondante ). N'oubliez pas de changer la fonction de comparaison en
GL_LEQUAL :
glDepthFunc(GL_LEQUAL);
Le shader de fragments sélectionne simplement à partir d'une carte cubique:
#version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); }
La sélection à partir de la carte est basée sur les coordonnées locales interpolées des sommets du cube, qui est la direction correcte de la sélection dans ce cas (encore une fois, discuté dans la leçon sur les skyboxes,
environ Per. ). Étant donné que les composants de transport dans la matrice de vue ont été ignorés, le rendu de la skybox ne dépendra pas de la position de l'observateur, créant l'illusion d'un arrière-plan infiniment éloigné. Comme ici, nous émettons directement les données de la carte HDR vers le tampon d'images par défaut, qui est le récepteur LDR, il est nécessaire de rappeler la compression tonale. Et enfin, presque toutes les cartes HDR sont stockées dans un espace linéaire, ce qui signifie que
la correction gamma doit être appliquée comme accord de traitement final.
Ainsi, lors de la sortie de la skybox obtenue, ainsi que du tableau de sphères déjà familier, quelque chose de similaire est obtenu:
Eh bien, beaucoup d'efforts ont été dépensés, mais à la fin, nous avons réussi à nous habituer à lire la carte d'environnement HDR, à la convertir d'une carte équilatérale en une carte cubique et à produire la carte cubique HDR en tant que skybox dans la scène. De plus, le code de conversion en carte cubique par rendu en six faces d'une carte cubique nous est utile plus loin dans la tâche de
convolution d'une carte d'environnement . Le code pour l'ensemble du processus de conversion est
ici .
Convolution d'une carte cubique
Comme cela a été dit au début de la leçon, notre objectif principal est de résoudre l'intégrale pour toutes les directions possibles de l'éclairage diffus indirect, en tenant compte de l'irradiation donnée de la scène sous la forme d'une carte cubique de l'environnement. On sait que l'on peut obtenir la valeur de la luminosité énergétique de la scène
L(p,wi) pour une direction arbitraire
wi en échantillonnant à partir du HDR une carte cubique de l'environnement dans cette direction. Pour résoudre l'intégrale, il sera nécessaire d'échantillonner la luminosité énergétique de la scène dans toutes les directions possibles de l'hémisphère
Omega chaque fragment examiné.
De toute évidence, la tâche d'échantillonner l'éclairage de l'environnement de toutes les directions possibles dans l'hémisphère
Omega est impraticable par calcul - il existe un nombre infini de telles directions. Cependant, il est possible d'appliquer l'approximation en prenant un nombre fini de directions choisies au hasard ou situées uniformément à l'intérieur de l'hémisphère.
Cela nous permettra d'obtenir une assez bonne approximation de l'irradiation vraie, résolvant essentiellement l'intégrale qui nous intéresse sous la forme d'une somme finie.Mais pour les tâches en temps réel, même une telle approche est toujours incroyablement imposée, car les échantillons sont prélevés pour chaque fragment, et le nombre d'échantillons doit être suffisamment élevé pour un résultat acceptable. Ainsi, il serait bien de préparer à l'avance les données de cette étape, en dehors du processus de rendu. Puisque l'orientation de l'hémisphère détermine à partir de quelle région de l'espace nous capturons l'irradiation, il est possible de calculer à l'avance l'irradiation pour chaque orientation possible de l'hémisphère sur la base de toutes les directions sortantes possiblesw o :L o ( p , ω o ) = k d cπ ∫ΩLi(p,ωi)n⋅ωidωi
Par conséquent, pour un vecteur arbitraire donné w i , nous pouvons échantillonner à partir de la carte d'irradiance calculée afin d'obtenir l'irradiance diffuse dans cette direction. Pour déterminer l'ampleur du rayonnement diffus indirect au point du fragment actuel, nous prenons l'irradiation totale d'un hémisphère orienté le long de la normale à la surface du fragment. En d'autres termes, obtenir l'irradiation d'une scène se résume à une simple sélection: vec3 irradiance = texture(irradianceMap, N);
De plus, pour créer une carte d'irradiation, il est nécessaire de convoluer la carte d'environnement, convertie en carte cubique. On sait que pour chaque fragment son hémisphère est considéré orienté selon la normale à la surfaceN .
Dans ce cas, la convolution de la carte cubique est réduite au calcul de la quantité moyenne de luminosité énergétique de toutes les directions w i à l' intérieurl'hémisphèreΩ orienté le long de la normaleN :Heureusement, le travail préliminaire fastidieux que nous avons effectué au début de la leçon rendra maintenant assez facile la conversion de la carte d'environnement en carte cubique dans un shader de fragment spécial, dont la sortie sera utilisée pour former une nouvelle carte cubique. Pour cela, le morceau de code qui a été utilisé pour traduire une carte d'environnement rectangulaire égale en une carte cubique est utile.Il ne reste plus qu'à prendre un autre shader de traitement: #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { // vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] // FragColor = vec4(irradiance, 1.0); }
Ici, l'échantillonneur environmentMap est une carte cubique HDR de l'environnement précédemment dérivée d'un équilatéral.Il existe de nombreuses façons de convoluer la carte d'environnement. Dans ce cas, pour chaque texel de la carte cubique, nous allons générer plusieurs vecteurs d'échantillonnage d'hémisphèreΩ , orienté le long de la direction de l'échantillon, et moyenne des résultats. Le nombre d'échantillons vecteurs sera fixe, et les vecteurs eux-mêmes seront répartis uniformément dans l'hémisphère. Je note que l'intégrande est une fonction continue, et une estimation discrète de cette fonction ne sera qu'une approximation. Et plus nous prendrons de vecteurs d'échantillonnage, plus nous serons proches de la solution analytique de l'intégrale.L'intégrande de l'expression de réflectivité dépend de l'angle solided w - valeurs avec lesquelles il n'est pas très pratique de travailler. Au lieu de s'intégrer sur un angle solided w on change l'expression, conduisant à l'intégration sur des coordonnées sphériquesθ et
ϕ :L'angle Phi représentera l'azimut dans le plan de la base de l'hémisphère, variant de 0 à 2 π .
Angle θ représentera l'angle d'élévation, variant de 0 à12 π .
L'expression modifiée de la réflectivité en ces termes est la suivante:L o ( p , ϕ o , θ o ) = k d cπ ∫ 2 π ϕ = 0 ∫ 12 πθ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ
La solution d'une telle intégrale nécessitera de prélever un nombre fini d'échantillons dans l'hémisphère Ω et la moyenne des résultats. Connaître le nombre d'échantillonsn 1 et
n 2 pour chacune des coordonnées sphériques, on peut traduire l'intégrale à lasomme riemannienne:L o ( p , ϕ o , θ o ) = k d cπ 1n 1 n 2 n 1 ∑ ϕ=0 n 2 ∑ θ=0Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ
Étant donné que les deux coordonnées sphériques varient discrètement, à chaque instant, l'échantillonnage est effectué avec une certaine zone moyenne dans l'hémisphère, comme on peut le voir sur la figure ci-dessus. En raison de la nature de la surface sphérique, la taille de la zone d'échantillonnage discrète diminue inévitablement avec l'augmentation de l'angle d'élévationθ et approchant du zénith. Pour compenser cet effet de réduction de la surface, nous avons ajouté un coefficient de poids à l'expressions i n θ .
En conséquence, la mise en œuvre d'un échantillonnage discret dans l'hémisphère basé sur des coordonnées sphériques pour chaque fragment sous forme de code est la suivante: vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { // . ( -) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); // vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples));
La variable sampleDelta détermine la taille du pas discret le long de la surface de l'hémisphère. En modifiant cette valeur, vous pouvez augmenter ou diminuer la précision du résultat.À l'intérieur des deux cycles, un vecteur d'échantillonnage tridimensionnel régulier est formé à partir de coordonnées sphériques, transféré de la tangente à l'espace mondial, puis utilisé pour échantillonner une carte d'environnement cubique à partir du HDR. Le résultat des échantillons est accumulé dans la variable d' irradiance , qui à la fin du traitement sera divisée par le nombre d'échantillons réalisés afin d'obtenir une valeur moyenne d'irradiation. Notez que le résultat de l'échantillonnage de la texture est modulé par deux quantités: cos (thêta) - pour prendre en compte l'atténuation de la lumière à de grands angles, et sin (thêta)- pour compenser la réduction de la zone d'échantillonnage à l'approche du zénith.Il ne reste plus qu'à gérer le code qui rend et capture les résultats de la convolution de la carte d' environnement envCubemap . Tout d'abord, créez une carte cubique pour stocker l'irradiation (vous devrez le faire une fois, avant d'entrer dans le cycle de rendu principal): unsigned int irradianceMap; glGenTextures(1, &irradianceMap); glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Étant donné que la carte d'irradiation est obtenue en faisant la moyenne d'échantillons uniformément distribués de la luminosité énergétique de la carte de l'environnement, elle ne contient pratiquement pas de parties et d'éléments à haute fréquence - une texture de résolution assez basse (32x32 ici) et un filtrage linéaire activé seront suffisants pour la stocker.Ensuite, définissez le tampon de capture sur cette résolution: glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
Le code de capture des résultats de convolution est similaire au code de transfert d'une carte d'environnement d'un équilatéral à un cubique, seul un shader de convolution est utilisé: irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap);
Après avoir terminé cette étape, nous aurons une carte d'irradiation précalculée sur nos mains qui peut être directement utilisée pour calculer l'illumination diffuse indirecte. Pour vérifier le déroulement de la convolution, nous allons essayer de remplacer la texture skybox de la carte d'environnement par la carte d'irradiation:Si, par conséquent, vous avez vu quelque chose qui ressemblait à une carte très floue de l'environnement, alors, très probablement, la convolution a réussi.PBR et éclairage indirect
La carte d'irradiation résultante est utilisée dans la partie diffuse de l'expression divisée de la réflectivité et représente la contribution accumulée de toutes les directions possibles de l'éclairage indirect. Puisque dans ce cas, la lumière ne provient pas de sources spécifiques, mais de l'environnement dans son ensemble, nous considérons l'éclairage indirect diffus et miroir comme arrière-plan ( ambiant ), remplaçant la valeur constante précédemment utilisée.Pour commencer, n'oubliez pas d'ajouter un nouvel échantillonneur avec une carte d'irradiation: uniform samplerCube irradianceMap;
Avoir une carte d'irradiation qui stocke toutes les informations sur le rayonnement diffus indirect de la scène et normal à la surface, obtenir des données sur l'irradiation d'un fragment particulier est aussi simple que de faire un échantillon à partir de la texture: // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb;
Cependant, comme le rayonnement indirect contient des données pour les composants diffus et miroir (comme nous l'avons vu dans la version composante de l'expression de la réflectivité), nous devons moduler la composante diffuse d'une manière spéciale. Comme dans la leçon précédente, nous utilisons l'expression de Fresnel pour déterminer le degré de réflexion de la lumière pour une surface donnée, d'où nous obtenons le degré de réfraction de la lumière ou le coefficient diffus: vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
Comme l'éclairage de fond tombe de toutes les directions dans l'hémisphère en fonction de la normale à la surface N , il est impossible de déterminer la seule médiane (àmi-chemin) vecteur de calcul du coefficient de Fresnel. Afin de simuler l'effet Fresnel dans de telles conditions, il est nécessaire de calculer le coefficient en fonction de l'angle entre la normale et le vecteur d'observation. Cependant, plus tôt, comme paramètre de calcul du coefficient de Fresnel, nous avons utilisé le vecteur médian obtenu à partir du modèle des microsurfaces et en fonction de la rugosité de surface. Étant donné que dans ce cas, la rugosité n'est pas incluse dans les paramètres de calcul, le degré de réflexion de la lumière par la surface sera toujours surestimé. L'éclairage indirect dans son ensemble devrait se comporter de la même manière que l'éclairage direct, c'est-à-dire des surfaces rugueuses, nous nous attendons à un degré de réflexion plus faible sur les bords. Mais comme la rugosité n'est pas prise en compte,puis le degré de réflexion spéculaire selon Fresnel pour l'éclairage indirect semble irréaliste sur des surfaces rugueuses non métalliques (dans l'image ci-dessous, l'effet décrit est exagéré pour plus de clarté):
Vous pouvez contourner cette nuisance en introduisant de la rugosité dans l'expression de Fremlin-Schlick, un processus décrit par Sébastien Lagarde : vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
Étant donné la rugosité de surface lors du calcul de l'ensemble de Fresnel, le code de calcul du composant d'arrière-plan prend la forme suivante: vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
Il s'est avéré que l'utilisation de l'éclairage basé sur l'image se résume intrinsèquement à un échantillon d'une carte cubique. Toutes les difficultés sont principalement liées à la préparation préliminaire et au transfert de la carte de l'environnement sur la carte d'irradiation.En prenant une scène familière d'une leçon sur les sources de lumière analytiques contenant un éventail de sphères avec une métallicité et une rugosité variables, et en ajoutant un éclairage de fond diffus de l'environnement, vous obtenez quelque chose comme ceci:Cela semble toujours étrange, car les matériaux avec un haut degré de métallicité nécessitent toujours une réflexion pour vraiment ressembler, hmm, au métal (les métaux ne reflètent pas l'éclairage diffus, après tout). Et dans ce cas, les seules réflexions obtenues à partir de sources lumineuses analytiques ponctuelles. Et pourtant, nous pouvons maintenant dire que les sphères semblent plus immergées dans l'environnement (particulièrement perceptibles lors du changement de cartes d'environnement), car les surfaces répondent désormais correctement à l'éclairage de fond de l'environnement de la scène.Le code source complet de la leçon est ici.. Dans la prochaine leçon, nous traiterons enfin de la seconde moitié de l'expression de la réflectivité, responsable de l'éclairage spéculaire indirect. Après cette étape, vous ressentirez vraiment la puissance de l'approche PBR dans l'éclairage.Matériel supplémentaire
PS : Nous avons un télégramme conf pour la coordination des transferts. Si vous avez un sérieux désir d'aider à la traduction, alors vous êtes les bienvenus!