Apprenez OpenGL. Leçon 6.4 - IBL. Exposition spéculaire

OGL3
Dans la leçon précédente, nous avons préparé notre modèle PBR pour travailler avec la méthode IBL - pour cela, nous devions préparer à l'avance une carte d'irradiation décrivant la partie diffuse de l'éclairage indirect. Dans cette leçon, nous ferons attention à la deuxième partie de l'expression de la réflectivité - le miroir:

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




Vous pouvez remarquer que le composant miroir Cook-Torrens (sous-expression avec un facteur ks ) n'est pas constant et dépend de la direction de la lumière incidente, ainsi que de la direction d'observation. La solution de cette intégrale pour toutes les directions possibles d'incidence de la lumière, ainsi que toutes les directions possibles d'observation en temps réel, n'est tout simplement pas faisable. Par conséquent, les chercheurs d'Epic Games ont proposé une approche appelée l' approximation à somme divisée , qui vous permet de préparer à l'avance des données pour la composante miroir, sous certaines conditions.

Dans cette approche, la composante miroir de l'expression de la réflectance est divisée en deux parties, qui peuvent être individuellement pré-convolutées puis combinées dans un shader PBR pour être utilisées comme source de rayonnement spéculaire indirect. Comme pour la génération de cartes d'irradiation, le processus de convolution reçoit une carte d'environnement HDR à son entrée.

Pour comprendre la méthode d'approximation à somme divisée, regardons à nouveau l'expression de la réflectivité, ne laissant que la sous-expression du composant miroir (la partie diffuse a été considérée séparément dans la leçon précédente ):

Lo(p, omegao)= int limits Omega(ks fracDFG4( omegao cdotn)( omegai cdotn)Li(p, omegai)n cdot omegaid omegai= int limits Omegafr(p, omegai, omegao)Li(p, omegai)n cdot omegaid omegai


Comme pour la préparation de la carte d'irradiation, cette intégrale n'est pas possible à résoudre en temps réel. Par conséquent, il est souhaitable de calculer de manière similaire la carte pour la composante miroir de l'expression de la réflectivité et, dans le cycle de rendu principal, d'effectuer une sélection simple à partir de cette carte en fonction de la normale à la surface. Cependant, tout n'est pas si simple: la carte d'irradiation a été obtenue relativement facilement du fait que l'intégrale ne dépendait que de  omegai , et la sous-expression constante de la composante diffuse lambertienne pourrait être retirée du signe de l'intégrale. Dans ce cas, l'intégrale dépend non seulement de  omegai qui est facile à comprendre à partir de la formule BRDF:

fr(p,wi,wo)= fracDFG4( omegao cdotn)( omegai cdotn)


L'expression sous l'intégrale dépend aussi de  omegao - pour deux vecteurs directionnels, il est presque impossible de sélectionner à partir d'une carte cubique préalablement préparée. Position du point p dans ce cas, vous ne pouvez pas prendre en compte - pourquoi cela a été discuté dans la leçon précédente. Calcul préliminaire de l'intégrale pour toutes les combinaisons possibles  omegai et  omegao impossible dans les tâches en temps réel.

La méthode du montant fractionné d'Epic Games résout ce problème en divisant le problème de calcul préliminaire en deux parties indépendantes, dont les résultats peuvent être combinés ultérieurement pour obtenir la valeur calculée finale. La méthode de somme fractionnée extrait deux intégrales de l'expression d'origine pour le composant miroir:

Lo(p, omegao)= int limits OmegaLi(p, omegai)d omegai int limits Omegafr(p, omegai, omegao)n cdot omegaid omegai


Le résultat du calcul de la première partie est généralement appelé une carte d'environnement pré-filtrée , et c'est une carte d'environnement soumise au processus de convolution spécifié par cette expression. Tout cela est similaire au processus d'obtention d'une carte d'irradiation, mais dans ce cas, la convolution est effectuée en tenant compte de la valeur de rugosité. Des valeurs de rugosité plus élevées conduisent à l'utilisation de vecteurs d'échantillonnage plus disparates dans le processus de convolution, ce qui donne des résultats plus flous. Le résultat de convolution pour chaque niveau de rugosité sélectionné suivant est stocké dans le niveau de mip suivant de la carte d'environnement préparée. Par exemple, une carte d'environnement, convolutée pour cinq niveaux de rugosité différents, contient cinq niveaux de mip et ressemble à ceci:


Les vecteurs d'échantillonnage et leur propagation sont déterminés sur la base de la fonction de distribution normale ( NDF ) du modèle BRDF de Cook-Torrens. Cette fonction accepte le vecteur normal et la direction d'observation comme paramètres d'entrée. La direction d'observation n'étant pas connue à l'avance lors du calcul préliminaire, les développeurs d'Epic Games ont dû faire une autre hypothèse: la direction du regard (et donc la direction de la réflexion spéculaire) est toujours identique à la direction de sortie de l'échantillon  omegao . Sous forme de code:

vec3 N = normalize(w_o); vec3 R = N; vec3 V = R; 

Dans de telles conditions, la direction du regard n'est pas requise dans le processus de convolution de la carte de l'environnement, ce qui rend le calcul réalisable en temps réel. Mais d'un autre côté, nous perdons la distorsion caractéristique des réflexions spéculaires lorsqu'elles sont observées à un angle aigu par rapport à la surface réfléchissante, comme on peut le voir dans l'image ci-dessous (de Moving Frostbite à PBR ). En général, un tel compromis est considéré comme acceptable.


La deuxième partie de l'expression à somme fractionnée contient le BRDF de l'expression d'origine pour le composant miroir. En supposant que la luminosité de l'énergie entrante est représentée spectralement par la lumière blanche dans toutes les directions (c.-à-d. L(p,x)=1,0 ), il est possible de pré-calculer la valeur de BRDF avec les paramètres d'entrée suivants: rugosité du matériau et angle entre la normale n et direction de la lumière  omegai (ou n cdot omegai ) L'approche Epic Games consiste à stocker les résultats du calcul BRDF pour chaque combinaison de rugosité et l'angle entre la normale et la direction de la lumière sous la forme d'une texture bidimensionnelle connue sous le nom de carte d'intégration BRDF , qui sera ensuite utilisée comme table de consultation ( LUT ). . Cette texture de référence utilise les canaux de sortie rouge et vert pour stocker l'échelle et le décalage pour calculer le coefficient de Fresnel de la surface, ce qui nous permet finalement de résoudre la deuxième partie de l'expression pour la somme séparée:


Cette texture auxiliaire est créée comme suit: les coordonnées de texture horizontales (allant de [0., 1.]) sont considérées comme des valeurs de paramètres d'entrée n cdot omegai Fonctions BRDF; les coordonnées de texture verticales sont considérées comme des valeurs de rugosité d'entrée.

Par conséquent, avec une telle carte d'intégration et une carte d'environnement prétraitée, vous pouvez combiner les échantillons à partir d'eux pour obtenir la valeur finale de l'expression intégrale du composant miroir:

 float lod = getMipLevelFromRoughness(roughness); vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod); vec2 envBRDF = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy; vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

Cet examen de la méthode de somme fractionnée d'Epic Games devrait vous donner une idée du processus d'approximation de la partie de l'expression de réflectance qui est responsable du composant miroir. Essayons maintenant de préparer nous-mêmes les données de la carte.

Préfiltrage de la carte d'environnement HDR


Le pré-filtrage de la carte de l'environnement est similaire à ce qui a été fait pour obtenir une carte d'irradiation. La seule différence est que nous prenons maintenant en compte la rugosité et enregistrons le résultat pour chaque niveau de rugosité dans le nouveau niveau de mip de la carte cubique.

Vous devez d'abord créer une nouvelle carte cubique qui contiendra le résultat du pré-filtrage. Afin de créer le nombre nécessaire de niveaux de mip, nous appelons simplement glGenerateMipmaps () - la mémoire nécessaire sera allouée pour la texture actuelle:

 unsigned int prefilterMap; glGenTextures(1, &prefilterMap); glBindTexture(GL_TEXTURE_CUBE_MAP, prefilterMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 128, 128, 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_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); 

Remarque: étant donné que la sélection dans prefilterMap sera basée sur l'existence de niveaux de mip, il est nécessaire de définir le mode de filtre de réduction sur GL_LINEAR_MIPMAP_LINEAR pour activer le filtrage trilinéaire. Les images prétraitées des images miroir sont stockées sur des faces distinctes de la carte cubique avec une résolution au niveau du mip de base de seulement 128x128 pixels. Pour la plupart des matériaux, cela suffit, cependant, si votre scène a un nombre accru de surfaces lisses et brillantes (par exemple, une voiture neuve), vous devrez peut-être augmenter cette résolution.

Dans la leçon précédente, nous avons convolutionné la carte de l'environnement en créant des vecteurs échantillons qui sont uniformément répartis dans l'hémisphère  Omega en utilisant des coordonnées sphériques. Pour obtenir l'irradiation, cette méthode est assez efficace, ce qui ne peut être dit des calculs de réflexions spéculaires. La physique des reflets spéculaires nous dit que la direction de la lumière réfléchie spéculaire est adjacente au vecteur de réflexion r pour une surface normale n même si la rugosité n'est pas nulle:


La forme généralisée des directions de réflexion sortantes possibles est appelée lobe spéculaire ( lobe spéculaire ; "pétale d'un diagramme de rayonnement miroir" - peut-être trop verbeux, environ Per. ). Avec une augmentation de la rugosité, le pétale se développe et se dilate. De plus, sa forme change en fonction de la direction de l'incidence de la lumière. Ainsi, la forme du pétale dépend fortement des propriétés du matériau.

Revenant au modèle des microsurfaces, on peut imaginer la forme du lobe miroir comme décrivant l'orientation de la réflexion par rapport au vecteur médian des micro-surfaces, en tenant compte d'une direction donnée de l'incidence lumineuse. Comprenant que la plupart des rayons de lumière réfléchie se trouvent à l'intérieur d'un pétale de miroir orienté sur la base du vecteur médian, il est logique de créer des vecteurs échantillons orientés de manière similaire. Sinon, beaucoup d'entre eux seront inutiles. Cette approche est appelée échantillonnage d'importance .

Intégration Monte Carlo et échantillonnage de signification


Pour bien comprendre la signification de l'échantillon en termes de signification, vous devrez d'abord vous familiariser avec un appareil mathématique tel que la méthode d'intégration de Monte Carlo. Cette méthode est basée sur une combinaison de statistiques et de théorie des probabilités et aide à résoudre numériquement un problème statistique sur un grand échantillon sans avoir à considérer chaque élément de cet échantillon.

Par exemple, vous souhaitez calculer la croissance démographique moyenne d'un pays. Pour obtenir un résultat précis et fiable, il faudrait mesurer la croissance de chaque citoyen et faire la moyenne du résultat. Cependant, comme la population de la plupart des pays est assez importante, cette approche est pratiquement irréalisable, car elle nécessite trop de ressources pour être exécutée.
Une autre approche consiste à créer un sous-échantillon plus petit rempli d'éléments vraiment aléatoires (sans biais) de l'échantillon d'origine. Ensuite, vous mesurez également la croissance et faites la moyenne du résultat pour ce sous-échantillon. Vous pouvez prendre au moins une centaine de personnes et obtenir un résultat, même s'il n'est pas absolument exact, mais tout de même assez proche de la situation réelle. L'explication de cette méthode réside dans la prise en compte de la loi des grands nombres. Et son essence est décrite de cette façon: le résultat d'une mesure dans un sous-échantillon de taille plus petit N , composé d'éléments véritablement aléatoires de l'ensemble d'origine, sera proche du résultat de contrôle des mesures effectuées sur l'ensemble initial. De plus, le résultat approximatif a tendance à être vrai avec la croissance N .
L'intégration de Monte Carlo est l'application de la loi des grands nombres pour résoudre les intégrales. Au lieu de résoudre l'intégrale, en tenant compte de l'ensemble (éventuellement infini) des valeurs x nous utilisons N des points d'échantillonnage aléatoires et la moyenne du résultat. Avec croissance N le résultat approximatif est garanti de se rapprocher de la solution exacte de l'intégrale.

O= int limitsbaf(x)dx= frac1N sumN1i=0 fracf(x)pdf(x)


Pour résoudre l'intégrale, nous obtenons la valeur de l'intégrande pour N des points aléatoires de l'échantillon dans [a, b], les résultats sont résumés et divisés par le nombre total de points pris pour la moyenne. Objet pdf décrit la fonction de densité de probabilité , qui montre la probabilité avec laquelle chaque valeur sélectionnée se produit dans l'échantillon d'origine. Par exemple, cette fonction pour la croissance des citoyens ressemblerait à ceci:


On peut voir qu'en utilisant des points d'échantillonnage aléatoires, nous avons une chance beaucoup plus élevée d'atteindre une valeur de croissance de 170 cm que quelqu'un avec une croissance de 150 cm.

Il est clair que lors de l'intégration de Monte Carlo, certains points d'échantillonnage sont plus susceptibles d'apparaître dans la séquence que d'autres. Par conséquent, dans toute expression d'estimation de Monte Carlo, nous divisons ou multiplions la valeur sélectionnée par la probabilité de son occurrence en utilisant la fonction de densité de probabilité. À l'heure actuelle, lors de l'évaluation de l'intégrale, nous avons créé de nombreux points d'échantillonnage uniformément répartis: la chance d'obtenir l'un d'eux était la même. Ainsi, notre estimation était non biaisée , ce qui signifie qu'à mesure que le nombre de points d'échantillonnage augmente, notre estimation converge vers la solution exacte de l'intégrale.

Cependant, certaines fonctions d'évaluation sont biaisées , c'est-à-dire impliquant la création de points d'échantillonnage non pas d'une manière vraiment aléatoire, mais avec une prédominance d'une certaine ampleur ou direction. De telles fonctions d'évaluation permettent à l'estimation de Monte Carlo de converger vers la solution exacte beaucoup plus rapidement . En revanche, en raison du biais de la fonction d'évaluation, la solution peut ne jamais converger. Dans le cas général, cela est considéré comme un compromis acceptable, en particulier dans les problèmes d'infographie, car l'estimation est très proche du résultat analytique et n'est pas requise si son effet semble visuellement assez fiable. Comme nous le verrons bientôt, l'échantillon par signification (en utilisant une fonction d'estimation biaisée) vous permet de créer des points d'échantillonnage biaisés dans une certaine direction, qui est pris en compte en multipliant ou en divisant chaque valeur sélectionnée par la valeur correspondante de la fonction de densité de probabilité.

L'intégration de Monte Carlo est assez courante dans les problèmes d'infographie, car c'est une méthode assez intuitive pour estimer la valeur des intégrales continues par une méthode numérique qui est assez efficace. Il suffit de prendre une zone ou un volume dans lequel l'échantillon est prélevé (par exemple, notre hémisphère  Omega ), créer N des points d'échantillonnage aléatoires se trouvant à l'intérieur, et effectuer une sommation pondérée des valeurs obtenues.

La méthode de Monte Carlo est un sujet de discussion très étendu, et ici nous n'entrerons plus dans les détails, mais il y a un détail plus important: il n'y a aucun moyen de créer des échantillons aléatoires . Par défaut, chaque point d'échantillonnage est complètement (psvedo) aléatoire - ce que nous attendons. Mais, en utilisant certaines propriétés de séquences quasi aléatoires, il est possible de créer des ensembles de vecteurs qui, bien qu'aléatoires, ont des propriétés intéressantes. Par exemple, lors de la création d'échantillons aléatoires pour le processus d'intégration, vous pouvez utiliser les séquences dites à faible écart , qui garantissent le caractère aléatoire des points d'échantillonnage créés, mais dans l'ensemble général, ils sont répartis plus uniformément:


L'utilisation de séquences à faible décalage pour créer un ensemble d'échantillons de vecteurs pour le processus d'intégration est la méthode d'intégration Quasi-Monte Carlo . Les quasi-méthodes de Monte Carlo convergent beaucoup plus rapidement que l'approche générale, ce qui est une propriété très attrayante pour les applications avec des exigences de performances élevées.

Nous connaissons donc la méthode générale et quasi-Monte Carlo, mais il y a un détail supplémentaire qui fournira un taux de convergence encore plus élevé: un échantillon par signification.
Comme déjà noté dans la leçon, pour les réflexions spéculaires, la direction de la lumière réfléchie est enfermée dans un lobe spéculaire, dont la taille et la forme dépendent de la rugosité de la surface réfléchissante. Comprendre que tous les vecteurs d'échantillons (quasi) aléatoires qui se trouvent à l'extérieur du lobe miroir n'affecteront pas l'expression intégrale de la composante miroir, c'est-à-dire inutile. Il est logique de focaliser la génération d'échantillons vecteurs dans la région du lobe miroir en utilisant la fonction d'estimation biaisée pour la méthode de Monte Carlo.

C'est l'essence même de l'échantillonnage par signification: la création de vecteurs d'échantillonnage est enfermée dans une certaine zone orientée le long du vecteur médian des micro-surfaces, et dont la forme est déterminée par la rugosité du matériau. En utilisant une combinaison de la quasi-méthode de Monte Carlo, des séquences de faible inadéquation et biais dans le processus de création de vecteurs d'échantillonnage en raison de l'échantillonnage significatif, nous atteignons des taux de convergence très élevés. Étant donné que la convergence vers la solution est suffisamment rapide, nous pouvons utiliser un plus petit nombre d'échantillons vecteurs pour obtenir une estimation suffisamment acceptable. La combinaison de méthodes décrite permet, en principe, aux applications graphiques de résoudre même l'intégrale du composant miroir en temps réel, bien que le calcul préliminaire reste une approche beaucoup plus rentable.

Séquence de faible décalage


Dans cette leçon, nous utilisons toujours un calcul préliminaire de la composante miroir de l'expression de la réflectance pour le rayonnement indirect. Et nous utiliserons un échantillon significatif en utilisant une séquence aléatoire de faible décalage et la quasi-méthode de Monte Carlo. La séquence utilisée est connue sous le nom de séquence Hammersley , dont Holger Dammertz donne une description détaillée. Cette séquence, à son tour, est basée sur la séquence de van der Corput , qui utilise une transformation binaire spéciale de la fraction décimale par rapport au point décimal.

En utilisant des astuces arithmétiques au niveau du bit, vous pouvez définir assez efficacement la séquence van der Corpute directement dans le shader et en fonction de cela créer le i-ème élément de la séquence Hammersley à partir de la sélection dans N articles:

 float RadicalInverse_VdC(uint bits) { bits = (bits << 16u) | (bits >> 16u); bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u); bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u); bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u); bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u); return float(bits) * 2.3283064365386963e-10; // / 0x100000000 } // ---------------------------------------------------------------------------- vec2 Hammersley(uint i, uint N) { return vec2(float(i)/float(N), RadicalInverse_VdC(i)); } 

Hammersley () renvoie le ième élément d'une séquence de faible décalage à partir d'échantillons de tailles multiples N .
Tous les pilotes OpenGL ne prennent pas en charge les opérations au niveau du bit (WebGL et OpenGL ES 2.0, par exemple), donc pour certains environnements, une implémentation alternative de leur utilisation peut être requise:

 float VanDerCorpus(uint n, uint base) { float invBase = 1.0 / float(base); float denom = 1.0; float result = 0.0; for(uint i = 0u; i < 32u; ++i) { if(n > 0u) { denom = mod(float(n), 2.0); result += denom * invBase; invBase = invBase / 2.0; n = uint(float(n) / 2.0); } } return result; } // ---------------------------------------------------------------------------- vec2 HammersleyNoBitOps(uint i, uint N) { return vec2(float(i)/float(N), VanDerCorpus(i, 2u)); } 

Je note qu'en raison de certaines restrictions sur les opérateurs de cycle dans l'ancien matériel, cette implémentation passe par les 32 bits. En conséquence, cette version n'est pas aussi productive que la première option - mais elle fonctionne sur n'importe quel matériel, et même en l'absence d'opérations sur les bits.

Échantillon d'importance dans le modèle GGX


Au lieu d'une distribution uniforme ou aléatoire (Monte Carlo) des vecteurs échantillons générés dans l'hémisphère  Omega , qui apparaît dans l'intégrale que nous résolvons, nous allons essayer de créer des vecteurs pour qu'ils gravitent dans la direction principale de réflexion de la lumière, caractérisée par le vecteur médian des microsurfaces et en fonction de la rugosité de surface. Le processus d'échantillonnage lui-même sera similaire à celui envisagé précédemment: ouvrir un cycle avec un nombre d'itérations suffisamment important, créer un élément d'une séquence de faible décalage, sur la base de celui-ci, nous créons un vecteur d'échantillonnage dans l'espace tangent, transférons ce vecteur aux coordonnées mondiales et utilisons la luminosité énergétique de la scène pour échantillonner. En principe, les changements ne concernent que le fait qu'un élément de la séquence de faible décalage est désormais utilisé pour spécifier un nouveau vecteur échantillon:

 const uint SAMPLE_COUNT = 4096u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); 

De plus, pour la formation complète du vecteur échantillon, il faudra en quelque sorte l'orienter en direction du lobe miroir correspondant à un niveau de rugosité donné. Vous pouvez prendre le NDF (fonction de distribution normale) d'une leçon théorique et combiner avec le NDF GGX pour la méthode de spécification d'un vecteur échantillon dans le domaine de la paternité Epic Games:

 vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness) { float a = roughness*roughness; float phi = 2.0 * PI * Xi.x; float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y)); float sinTheta = sqrt(1.0 - cosTheta*cosTheta); //       vec3 H; Hx = cos(phi) * sinTheta; Hy = sin(phi) * sinTheta; Hz = cosTheta; //        vec3 up = abs(Nz) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 tangent = normalize(cross(up, N)); vec3 bitangent = cross(N, tangent); vec3 sampleVec = tangent * Hx + bitangent * Hy + N * Hz; return normalize(sampleVec); } 

Le résultat est un vecteur échantillon, approximativement orienté le long du vecteur médian des microsurfaces, pour une rugosité donnée et un élément de la séquence de faible mésappariement Xi . Notez qu'Epic Games utilise le carré de la valeur de rugosité pour une meilleure qualité visuelle, basée sur le travail original de Disney sur la méthode PBR.

Après avoir terminé la mise en œuvre de la séquence de Hammersley et de l'exemple de code de génération de vecteur, nous pouvons donner le code de pré-filtrage et de shader de convolution:

 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; uniform float roughness; const float PI = 3.14159265359; float RadicalInverse_VdC(uint bits); vec2 Hammersley(uint i, uint N); vec3 ImportanceSampleGGX(vec2 Xi, vec3 N, float roughness); void main() { vec3 N = normalize(localPos); vec3 R = N; vec3 V = R; const uint SAMPLE_COUNT = 1024u; float totalWeight = 0.0; vec3 prefilteredColor = vec3(0.0); for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(dot(N, L), 0.0); if(NdotL > 0.0) { prefilteredColor += texture(environmentMap, L).rgb * NdotL; totalWeight += NdotL; } } prefilteredColor = prefilteredColor / totalWeight; FragColor = vec4(prefilteredColor, 1.0); } 


Nous effectuons un filtrage préliminaire de la carte d'environnement en fonction d'une rugosité donnée, dont le niveau change pour chaque niveau de mip de la carte cubique résultante (de 0,0 à 1,0), et le résultat du filtre est stocké dans la variable prefilteredColor . Ensuite, la variable est divisée par le poids total pour l'ensemble de l'échantillon, et les échantillons avec une contribution plus faible au résultat final (ayant une valeur NdotL inférieure ) augmentent également le poids total moins.

Enregistrement des données de pré-filtrage dans les niveaux de mip


Il reste à écrire du code qui demande directement à OpenGL de filtrer la carte d'environnement avec différents niveaux de rugosité, puis d'enregistrer les résultats dans une série de niveaux de mip de la carte cubique cible. Ici, le code déjà préparé de la leçon sur le calcul de la carte d' irradiation est utile :

 prefilterShader.use(); prefilterShader.setInt("environmentMap", 0); prefilterShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); unsigned int maxMipLevels = 5; for (unsigned int mip = 0; mip < maxMipLevels; ++mip) { //        - unsigned int mipWidth = 128 * std::pow(0.5, mip); unsigned int mipHeight = 128 * std::pow(0.5, mip); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mipWidth, mipHeight); glViewport(0, 0, mipWidth, mipHeight); float roughness = (float)mip / (float)(maxMipLevels - 1); prefilterShader.setFloat("roughness", roughness); for (unsigned int i = 0; i < 6; ++i) { prefilterShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, prefilterMap, mip); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); } } glBindFramebuffer(GL_FRAMEBUFFER, 0); 

Le processus est similaire à une convolution de la carte d'irradiation, mais cette fois, vous devez spécifier la taille du tampon de trame à chaque étape, en la réduisant de moitié pour correspondre aux niveaux de mip. De plus, le niveau de mip auquel le rendu sera effectué en ce moment doit être spécifié comme paramètre de la fonction glFramebufferTexture2D () .

Le résultat de l'exécution de ce code devrait être une carte cubique contenant des images de réflexions de plus en plus floues à chaque niveau de mip suivant. Vous pouvez utiliser une telle carte cubique comme source de données pour skybox et prendre un échantillon de n'importe quel niveau de mip en dessous de zéro:

 vec3 envColor = textureLod(environmentMap, WorldPos, 1.2).rgb; 

Le résultat de cette action sera l'image suivante:


Il ressemble à une carte d'environnement source très floue. Si votre résultat est similaire, le processus de filtrage préliminaire de la carte d'environnement HDR est probablement effectué correctement. Essayez d'expérimenter avec un échantillon de différents niveaux de mip et observez une augmentation progressive du flou à chaque niveau suivant.

Préfiltre des artefacts de convolution


Pour la plupart des tâches, l'approche décrite fonctionne plutôt bien, mais tôt ou tard vous devrez rencontrer divers artefacts générés par le processus de préfiltrage. Voici les méthodes les plus courantes et les traiter.

La manifestation des coutures de la carte cubique


La sélection des valeurs de la carte en cubes traitée par le filtre préliminaire pour les surfaces à rugosité élevée conduit à lire les données du niveau de mip quelque part plus près de la fin de leur chaîne. Lors de l'échantillonnage à partir d'une carte cubique, OpenGL par défaut n'interpole pas linéairement entre les faces de la carte cubique. Étant donné que les niveaux de mip élevés ont une résolution inférieure et que la carte de l'environnement a été convolue en tenant compte d'un lobe miroir beaucoup plus grand, l' absence de filtrage de texture entre les faces devient évidente:


Heureusement, OpenGL a la possibilité d'activer ce filtrage avec un simple indicateur:

 glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS); 

Il suffit de définir l'indicateur quelque part dans le code d'initialisation de l'application et cet artefact est supprimé.

Des points lumineux apparaissent


Étant donné que les réflexions miroir dans le cas général contiennent des détails à haute fréquence, ainsi que des régions avec des luminosités très différentes, leur convolution nécessite l'utilisation d'un grand nombre de points d'échantillonnage pour prendre correctement en compte la grande dispersion des valeurs à l'intérieur des réflexions HDR de l'environnement. Dans l'exemple, nous prenons déjà un nombre suffisamment important d'échantillons, mais pour certaines scènes et niveaux élevés de rugosité du matériau cela ne sera toujours pas suffisant, et vous serez témoin de l'apparition de nombreux points autour des zones claires:


Vous pouvez continuer à augmenter le nombre d'échantillons, mais ce ne sera pas une solution universelle et dans certaines circonstances, cela autorisera toujours un artefact. Mais vous pouvez vous tourner vers la méthode Chetan Jags , qui vous permet de réduire la manifestation d'un artefact. Pour ce faire, au stade de convolution préliminaire, la sélection à partir de la carte d'environnement n'est pas effectuée directement, mais à partir d'un de ses niveaux de mip, en fonction de la valeur obtenue à partir de la fonction de distribution de probabilité de l'intégrande et de la rugosité:

 float D = DistributionGGX(NdotH, roughness); float pdf = (D * NdotH / (4.0 * HdotV)) + 0.0001; //        float resolution = 512.0; float saTexel = 4.0 * PI / (6.0 * resolution * resolution); float saSample = 1.0 / (float(SAMPLE_COUNT) * pdf + 0.0001); float mipLevel = roughness == 0.0 ? 0.0 : 0.5 * log2(saSample / saTexel); 

N'oubliez pas d'activer le filtrage trilinéaire pour la carte d'environnement afin de sélectionner avec succès parmi les niveaux de mip:

 glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 

N'oubliez pas non plus de créer directement des niveaux de mip pour la texture à l'aide d'OpenGL, mais uniquement après que le niveau de mip principal est complètement formé:

 //  HDR      ... [...] //  - glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); glGenerateMipmap(GL_TEXTURE_CUBE_MAP); 

Cette méthode fonctionne étonnamment bien, supprimant presque tous (et souvent tous) les points sur la carte filtrée, même à des niveaux de rugosité élevés.

Calcul préliminaire de BRDF


Nous avons donc traité avec succès la carte de l'environnement avec le filtre et nous pouvons maintenant nous concentrer sur la deuxième partie de l'approximation sous la forme d'une somme distincte représentant BRDF. Pour rafraîchir la mémoire, jetez un coup d'œil à l'enregistrement complet de la solution approximative:

L o ( p , ω o ) = Ω L i ( p , ω i ) d ω iΩ f r ( p , ω i , ω o ) n ω i d ω i


Nous avons calculé au préalable la partie gauche de la somme et enregistré les résultats pour différents niveaux de rugosité sur une carte cubique distincte. Le côté droit nécessitera la convolution de l'expression BDRF ainsi que les paramètres suivants: anglen ω i , rugosité de surface et coefficient de FresnelF 0 . Un processus similaire à l'intégration d'un BRDF en miroir pour un environnement complètement blanc ou avec une luminosité d'énergie constante L i = 1,0 . Convolution de BRDF pour trois variables n'est pas une tâche triviale, mais dans ce cas F 0 peut être dérivé de l'expression décrivant le miroir BRDF:

Ω fr(p,ωi,ωo)nωidωi=Ω fr(p,ωi,ωo) F ( ω o , h )F ( ω o , h ) nωidωi


Ici F est une fonction qui décrit le calcul de l'ensemble de Fresnel. En déplaçant le diviseur dans l'expression de BRDF, vous pouvez passer à la notation équivalente suivante:

Ω f r ( p , ω i , ω o )F ( ω o , h ) F(ωo,h)nωidωi


Remplacement de la bonne entrée F sur l'approximation de Fresnel-Schlick, on obtient:

Ω f r ( p , ω i , ω o )F ( ω o , h ) (F0+(1-F0)(1-ωoh)5)nωidωi


Indique l'expression ( 1 - ω oh ) 5 comment / a l p h a pour simplifier la décision surF 0 :

Ωfr(p,ωi,ωo)F(ωo,h)(F0+(1F0)α)nωidωi


Ωfr(p,ωi,ωo)F(ωo,h)(F0+1αF0α)nωidωi


Ωfr(p,ωi,ωo)F ( ω o , h ) (F0(1-α)+α)nωidωi


Fonction suivante Nous divisons F en deux intégrales:

Ω f r ( p , ω i , ω o )F ( ω o , h ) (F0(1-α))nωidωi+Ωfr(p,ωi,ωo)F ( ω o , h ) (α)nωidωi


De cette façon F 0 sera constant sous l'intégrale, et nous pouvons le retirer du signe de l'intégrale. Ensuite, nous révéleronsα dans l'expression d'origine et obtenez l'entrée finale pour BRDF sous la forme d'une somme distincte:

F0Ωfr(p,ωi,ωo)(1(1ωoh)5)nωidωi+Ωfr(p,ωi,ωo)(1ωoh)5nωidωi


Les deux intégrales résultantes représentent l'échelle et le décalage de la valeur F 0 en conséquence. Notez que f ( p , ω i , ω o ) contient une occurrenceF , car ces occurrences s'annulent et disparaissent de l'expression. En utilisant l'approche déjà développée, la convolution BRDF peut être réalisée avec les données d'entrée: rugosité et angle entre vecteurs

n et w o .Écrire le résultat dans la texture 2D - complexation carte BRDF ( carte d'intégration BRDF ), qui servira de valeurs de la table auxiliaire pour utilisation dans le shader finale, qui formeront le résultat final de l'éclairage spéculaire indirect.

Le shader de convolution BRDF fonctionne sur le plan, en utilisant directement les coordonnées de texture bidimensionnelles comme paramètres d'entrée du processus de convolution ( NdotV et rugosité ). Le code est sensiblement similaire à la convolution du pré-filtrage, mais ici le vecteur échantillon est traité en tenant compte de la fonction géométrique BRDF et de l'expression de l'approximation de Fresnel-Schlick:

 vec2 IntegrateBRDF(float NdotV, float roughness) { vec3 V; Vx = sqrt(1.0 - NdotV*NdotV); Vy = 0.0; Vz = NdotV; float A = 0.0; float B = 0.0; vec3 N = vec3(0.0, 0.0, 1.0); const uint SAMPLE_COUNT = 1024u; for(uint i = 0u; i < SAMPLE_COUNT; ++i) { vec2 Xi = Hammersley(i, SAMPLE_COUNT); vec3 H = ImportanceSampleGGX(Xi, N, roughness); vec3 L = normalize(2.0 * dot(V, H) * H - V); float NdotL = max(Lz, 0.0); float NdotH = max(Hz, 0.0); float VdotH = max(dot(V, H), 0.0); if(NdotL > 0.0) { float G = GeometrySmith(N, V, L, roughness); float G_Vis = (G * VdotH) / (NdotH * NdotV); float Fc = pow(1.0 - VdotH, 5.0); A += (1.0 - Fc) * G_Vis; B += Fc * G_Vis; } } A /= float(SAMPLE_COUNT); B /= float(SAMPLE_COUNT); return vec2(A, B); } // ---------------------------------------------------------------------------- void main() { vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y); FragColor = integratedBRDF; } 

Comme vous pouvez le voir, la convolution de BRDF est implémentée comme un arrangement presque littéral des calculs mathématiques ci-dessus. Les paramètres d'entrée de rugosité et d'angle sont pris.θ , un vecteur échantillon est formé sur la base de l'échantillon par signification, traité à l'aide de la fonction de géométrie et de l'expression de Fresnel transformée pour BRDF. Par conséquent, pour chaque échantillon, l'ampleur de la mise à l'échelle et du déplacement de la valeurF 0 , qui à la fin sont moyennés et renvoyés sous la formevec2. Dans uneleçonthéorique, il a été mentionné que la composante géométrique de BRDF est légèrement différente dans le cas du calcul de l'IBL, car le coefficient

k est spécifié différemment:

k d i r e c t = ( α + 1 ) 28


k I B L = α 22


Puisque la convolution BRDF fait partie de la solution de l'intégrale dans le cas du calcul d'IBL, nous utiliserons le coefficient k I B L pour le calcul de la fonction géométrique dans le modèle Schlick-GGX:

 float GeometrySchlickGGX(float NdotV, float roughness) { float a = roughness; float k = (a * a) / 2.0; float nom = NdotV; float denom = NdotV * (1.0 - k) + k; return nom / 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; } 

Veuillez noter que le coefficient k est calculé en fonction du paramètrea. De plus, dans ce cas, le paramètre derugositén'estpas au carré lors de la description du paramètrea, ce qui a été fait à d'autres endroits où ce paramètre a été appliqué. Je ne sais pas où se situe le problème: dans le travail d'Epic Games ou dans le travail initial de Disney, mais il convient de dire que c'est précisément cette affectation directe de la valeur derugositéauparamètrea quicrée la carte d'intégration BRDF identique, présentée dans la publication Epic Games.De plus, les résultats de convolution BRDF seront enregistrés sous la forme d'une texture 2D de taille 512x512:



 unsigned int brdfLUTTexture; glGenTextures(1, &brdfLUTTexture); //   ,     glBindTexture(GL_TEXTURE_2D, brdfLUTTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RG16F, 512, 512, 0, GL_RG, GL_FLOAT, 0); 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); 

Comme recommandé par Epic Games, un format de texture à virgule flottante 16 bits est utilisé ici. Veillez à définir le mode de répétition sur GL_CLAMP_TO_EDGE afin d'éviter d'échantillonner des artefacts à partir du bord.

Ensuite, nous utilisons le même objet tampon de trame et exécutons un shader sur la surface d'un quad en plein écran:

 glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brdfLUTTexture, 0); glViewport(0, 0, 512, 512); brdfShader.use(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); RenderQuad(); glBindFramebuffer(GL_FRAMEBUFFER, 0); 

En conséquence, nous obtenons une carte de texture qui stocke le résultat de la convolution de la partie de l'expression du montant fractionné responsable de BRDF:


Ayant en main les résultats du filtrage préliminaire de la carte de l'environnement et de la texture avec les résultats de la convolution BRDF, nous pouvons restituer le résultat du calcul de l'intégrale pour l'éclairage spéculaire indirect basé sur l'approximation par une somme séparée. La valeur restaurée sera ensuite utilisée comme rayonnement spéculaire indirect ou de fond.

Calcul de la réflectance finale dans le modèle IBL


Ainsi, afin d'obtenir une valeur décrivant la composante miroir indirect dans l'expression générale de la réflectivité, il est nécessaire de «coller» les composantes d'approximation calculées en un seul ensemble comme une somme distincte. Tout d'abord, ajoutez les échantillonneurs appropriés au shader final pour les données précalculées:

 uniform samplerCube prefilterMap; uniform sampler2D brdfLUT; 

Tout d'abord, nous obtenons la valeur de la réflexion spéculaire indirecte sur la surface en échantillonnant à partir d'une carte d'environnement prétraitée basée sur le vecteur de réflexion. Veuillez noter qu'ici, la sélection du niveau de mip pour l'échantillonnage est basée sur la rugosité de la surface. Pour les surfaces plus rugueuses, la réflexion sera plus floue :

 void main() { [...] vec3 R = reflect(-V, N); const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; [...] } 

Au stade de convolution préliminaire, nous n'avons préparé que 5 niveaux de mip (de zéro à quatrième), la constante MAX_REFLECTION_LOD sert à limiter la sélection à partir des niveaux de mip générés.

Ensuite, nous faisons une sélection à partir de la carte d'intégration BRDF en fonction de la rugosité et de l'angle entre la normale et la direction de la vue:

 vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); 

La valeur obtenue à partir de la carte contient des facteurs d'échelle et de déplacement pour F 0 (ici on prend la valeurF- coefficient de Fresnel). La valeur convertieF estensuite combinée avec la valeur obtenue à partir de la carte de préfiltrage pour obtenir une solution approximative de l'expression intégrale d'origine -spéculaire.Ainsi, nous obtenons une solution pour la partie de l'expression de la réflectivité, qui est responsable de la réflexion spéculaire. Pour obtenir une solution complète du modèle PBR IBL, vous devez combiner cette valeur avec la solution pour la partie diffuse de l'expression de réflectance que nous avons reçue dans ladernièreleçon:



 vec3 F = FresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kS = F; vec3 kD = 1.0 - kS; kD *= 1.0 - metallic; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; const float MAX_REFLECTION_LOD = 4.0; vec3 prefilteredColor = textureLod(prefilterMap, R, roughness * MAX_REFLECTION_LOD).rgb; vec2 envBRDF = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).rg; vec3 specular = prefilteredColor * (F * envBRDF.x + envBRDF.y); vec3 ambient = (kD * diffuse + specular) * ao; 

Je note que la valeur spéculaire n'est pas multipliée par kS , car elle contient déjà un coefficient de Fresnel.

Lançons notre application de test avec un ensemble familier de sphères aux caractéristiques changeantes de métallicité et de rugosité et examinons leur apparence dans toute la splendeur du PBR:


Vous pouvez aller encore plus loin et télécharger un ensemble de textures correspondant au modèle PBR et obtenir des sphères à partir de matériaux réels :


Ou même téléchargez un magnifique modèle avec les textures PBR préparées d' Andrew Maximov :


Je pense que vous n'aurez à convaincre personne que le modèle d'éclairage actuel semble beaucoup plus convaincant. De plus, l'éclairage semble physiquement correct quelle que soit la carte de l'environnement. Ci-dessous sont utilisées plusieurs cartes d'environnement HDR complètement différentes qui changent complètement la nature de l'éclairage - mais toutes les images semblent physiquement fiables, malgré le fait que vous n'ayez pas eu à régler de paramètres dans le modèle! (En principe, cette simplification du travail avec les matériaux est le principal avantage du pipeline PBR, et une meilleure image peut être considérée comme une conséquence agréable. Remarque. )


Fuh, notre voyage dans l'essence du moteur de rendu PBR est assez volumineux. Nous sommes arrivés au résultat à travers toute une série d'étapes et, bien sûr, beaucoup de choses peuvent mal tourner avec les premières approches. Par conséquent, pour tout problème, je vous conseille de bien comprendre l'exemple de code pour les sphères monochromes et texturées (et le code shader, bien sûr!). Ou demandez conseil dans les commentaires.

Et ensuite?


J'espère qu'en lisant ces lignes, vous avez déjà développé une compréhension du travail du modèle de rendu PBR, ainsi que compris et lancé avec succès une application de test. Dans ces leçons, nous avons calculé toutes les cartes de texture auxiliaires nécessaires pour le modèle PBR dans notre application avant le cycle de rendu principal. Pour les tâches de formation, cette approche est appropriée, mais pas pour une application pratique. Tout d'abord, une telle préparation préliminaire ne devrait avoir lieu qu'une seule fois, et non pas à chaque lancement d'application. Deuxièmement, si vous décidez d'ajouter des cartes d'environnement supplémentaires, vous devrez également les traiter au démarrage. Et si quelques cartes supplémentaires sont ajoutées? Véritable boule de neige.

C'est pourquoi, dans le cas général, une carte d'irradiation et une carte d'environnement prétraitées sont préparées une fois, puis enregistrées sur disque (la carte d'agrégation BRDF ne dépend pas de la carte d'environnement, de sorte qu'elle peut généralement être calculée ou téléchargée une fois). Il s'ensuit que vous aurez besoin d'un format pour stocker les cartes cubiques HDR, y compris leurs niveaux de mip. Eh bien, ou vous pouvez les stocker et les charger en utilisant l'un des formats les plus utilisés (donc .dds prend en charge l'enregistrement des niveaux de mip).

Un autre point important: afin de donner une compréhension approfondie du pipeline PBR dans ces leçons, j'ai décrit le processus complet de préparation du rendu PBR, y compris les calculs préliminaires des cartes auxiliaires pour IBL. Cependant, dans votre pratique, vous pouvez tout aussi bien utiliser l'un des grands utilitaires qui préparent ces cartes pour vous: par exemple cmftStudio ou IBLBaker .

On n'a pas pris en compte le processus de préparation de la carte de cube réflexions échantillons ( sondes de réflexion) et les processus connexes d'interpolation de carte cubique et de correction de parallaxe. En bref, cette technique peut être décrite comme suit: nous plaçons dans notre scène de nombreux objets de réflexion, qui forment une image environnementale locale sous la forme d'une carte cubique, puis toutes les cartes auxiliaires nécessaires pour le modèle IBL sont formées sur sa base. En interpolant les données de plusieurs échantillons en fonction de la distance de la caméra, vous pouvez obtenir un éclairage très détaillé basé sur l'image, dont la qualité n'est essentiellement limitée que par le nombre d'échantillons que nous sommes prêts à placer dans la scène. Cette approche vous permet de modifier correctement l'éclairage, par exemple, lorsque vous passez d'une rue fortement éclairée au crépuscule d'une certaine pièce. Je vais probablement écrire une leçon sur les tests de réflexion à l'avenir,Cependant, pour le moment, je ne peux que recommander l'article Chetan Jags ci-dessous pour examen.

(L'implémentation d'échantillons, et bien plus, peut être trouvée dans le moteur brut de l'auteur des tutoriels ici , environ Per. )

Matériel supplémentaire


  1. Real Shading dans Unreal Engine 4 : Une explication de l'approche d'Epic Games pour approximer l'expression du composant miroir par une somme divisée. Sur la base de cet article, le code de la leçon IBL PBR a été écrit.
  2. Ombrage basé physiquement et éclairage basé sur l'image : Un excellent article décrivant le processus d'inclusion du calcul du composant miroir d'IBL dans une application de pipeline interactif PBR.
  3. Éclairage basé sur l'image : Un article très long et détaillé sur l'IBL spéculaire et les problèmes connexes, y compris le problème d'interpolation de la sonde lumineuse.
  4. Moving Frostbite to PBR : , PBR «AAA».
  5. Physically Based Rendering – Part Three : , IBL PBR JMonkeyEngine.
  6. Implementation Notes: Runtime Environment Map Filtering for Image Based Lighting : HDR , .

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


All Articles