Apprenez OpenGL. Leçon 5.9 - Rendu différé


Dans les articles précédents, nous utilisions l'éclairage direct (rendu avant ou ombrage avant) . Il s'agit d'une approche simple dans laquelle nous dessinons un objet en tenant compte de toutes les sources de lumière, puis dessinons l'objet suivant avec tout l'éclairage sur celui-ci, et ainsi de suite pour chaque objet. C'est assez simple à comprendre et à mettre en œuvre, mais en même temps cela se révèle assez lentement du point de vue des performances: pour chaque objet il faut trier toutes les sources lumineuses. De plus, l'éclairage direct fonctionne de manière inefficace sur les scènes avec un grand nombre d'objets se chevauchant, car la plupart des calculs de pixel shader ne sont pas utiles et seront remplacés par des valeurs pour des objets plus proches.


Un éclairage ou un ombrage différé ou un rendu différé contourne ce problème et modifie considérablement la façon dont nous dessinons les objets. Cela donne de nouvelles opportunités d'optimiser considérablement les scènes avec un grand nombre de sources lumineuses, vous permettant de dessiner des centaines voire des milliers de sources lumineuses à une vitesse acceptable. Ci-dessous, une scène avec 1847 sources ponctuelles de lumière dessinées en utilisant un éclairage différé (image gracieuseté de Hannes Nevalainen). Quelque chose comme ça serait impossible avec un calcul direct de l'éclairage:


img1



L'idée de l'éclairage différé est que nous reportons les pièces les plus complexes sur le plan des calculs (comme l'éclairage) pour plus tard. L'éclairage différé se compose de deux passes: dans la première passe, la passe de géométrie (passe de géométrie) , la scène entière est dessinée et diverses informations sont stockées dans un ensemble de textures appelé le G-buffer. Par exemple: positions, couleurs, normales et / ou symétrie de surface pour chaque pixel. Les informations graphiques stockées dans le G-buffer sont ensuite utilisées pour calculer l'éclairage. Voici le contenu du G-buffer pour une trame:


img2


Dans le deuxième passage, appelé passage d'éclairage, nous utilisons les textures du tampon G lorsque nous dessinons le rectangle plein écran. Au lieu d'utiliser des shaders de vertex et de fragments séparément pour chaque objet, nous dessinons la scène entière pixel par pixel. Le calcul de l'éclairage reste exactement le même qu'avec un passage direct, mais nous prenons les données nécessaires uniquement à partir du G-buffer et des shaders variables (uniformes) , et non à partir du vertex shader.


L'image ci-dessous montre bien le processus de dessin général.


img3


Le principal avantage est que les informations stockées dans le G-buffer appartiennent aux fragments les plus proches qui ne sont cachés par rien: le test de profondeur ne les laisse que. Grâce à cela, nous calculons l'éclairage pour chaque pixel une seule fois, sans faire trop de travail. De plus, l'éclairage différé nous offre des possibilités d'optimisations supplémentaires, nous permettant d'utiliser beaucoup plus de sources lumineuses qu'en éclairage direct.


Cependant, il y a quelques inconvénients: le G-buffer stocke une grande quantité d'informations sur la scène. De plus, les données de type de position doivent être stockées avec une grande précision, par conséquent, le G-buffer prend un peu d'espace mémoire. Un autre inconvénient est que nous ne pourrons pas utiliser d'objets translucides (car le tampon stocke les informations uniquement pour la surface la plus proche) et l'anti-aliasing comme MSAA ne fonctionnera pas non plus. Il existe plusieurs solutions pour résoudre ces problèmes, elles sont discutées à la fin de l'article.


(Remarque: le tampon G prend beaucoup d'espace mémoire. Par exemple, pour un écran de 1920 * 1080 et utilisant 128 bits par pixel, le tampon prendra 33 Mo. Les besoins en bande passante mémoire sont de plus en plus importants - beaucoup plus de données sont écrites et lues)


G-buffer


G-buffer fait référence aux textures utilisées pour stocker les informations relatives à l'éclairage utilisées lors de la dernière passe de rendu. Voyons quelles informations nous avons besoin pour calculer l'éclairage pour le rendu direct:


  • Vecteur de position 3D: utilisé pour connaître la position du fragment par rapport à la caméra et aux sources lumineuses.
  • Couleur diffuse du fragment (réflectivité pour le rouge, le vert et le bleu - en général, la couleur).
  • Vecteur normal 3D (pour déterminer à quel angle la lumière tombe sur la surface)
  • flotteur pour stocker le composant miroir
  • La position de la source lumineuse et sa couleur.
  • Position de la caméra.

En utilisant ces variables, nous pouvons calculer la couverture en utilisant le modèle de Blinn-Fong que nous connaissons déjà. La couleur et la position de la source lumineuse, ainsi que la position de la caméra peuvent être des variables communes, mais les autres valeurs seront différentes pour chaque fragment d'image. Si nous transmettons exactement les mêmes données dans la passe finale d'éclairage différé, que nous utiliserions pour une passe directe, nous obtiendrons le même résultat, malgré le fait que nous dessinerons des fragments sur un rectangle 2D régulier.


OpenGL n'a aucune restriction sur ce que nous pouvons stocker dans une texture, il est donc logique de stocker toutes les informations dans une ou plusieurs textures de la taille d'un écran (appelé G-buffer) et de les utiliser toutes dans la passe d'éclairage. Étant donné que la taille des textures et de l'écran est la même, nous obtenons les mêmes données d'entrée qu'en éclairage direct.


En pseudo-code, l'image générale ressemble à ceci:


while(...) // render loop { // 1.  :  /    g- glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); gBufferShader.use(); for(Object obj : Objects) { ConfigureShaderTransformsAndUniforms(); obj.Draw(); } // 2.  :  g-     glBindFramebuffer(GL_FRAMEBUFFER, 0); glClear(GL_COLOR_BUFFER_BIT); lightingPassShader.use(); BindAllGBufferTextures(); SetLightingUniforms(); RenderQuad(); } 

Informations nécessaires pour chaque pixel: vecteur de position, vecteur normal, vecteur de couleur et valeur pour le composant miroir . Dans la passe géométrique, nous dessinons tous les objets de la scène et enregistrons toutes ces données dans le G-buffer. Nous pouvons utiliser plusieurs cibles de rendu pour remplir tous les tampons en un seul tirage, cette approche a été discutée dans l'article précédent sur la mise en œuvre de l'éclat: Bloom , traduction sur le hub


Pour la passe géométrique, créez un framebuffer avec le nom évident gBuffer, auquel nous attacherons plusieurs tampons de couleur et un tampon de profondeur. Pour stocker les positions et les normales, il est préférable d'utiliser une texture avec une grande précision (valeurs flottantes 16 ou 32 bits pour chaque composant), nous stockons la couleur diffuse et les valeurs spéculaires dans la texture par défaut (8 bits par précision du composant).


 unsigned int gBuffer; glGenFramebuffers(1, &gBuffer); glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); unsigned int gPosition, gNormal, gColorSpec; //   glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0); //   glGenTextures(1, &gNormal); glBindTexture(GL_TEXTURE_2D, gNormal); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0); //    +    glGenTextures(1, &gAlbedoSpec); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gAlbedoSpec, 0); //  OpenGL,        unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 }; glDrawBuffers(3, attachments); //           . [...] 

Puisque nous utilisons plusieurs objectifs de rendu, nous devons explicitement dire à OpenGL à quels tampons du GBuffer attaché nous allons dessiner dans glDrawBuffers() . Il convient également de noter que nous stockons les positions et les normales ont chacune 3 composants, et nous les stockons dans des textures RVB. Mais en même temps, nous mettons immédiatement dans la même texture RGBA à la fois la couleur et le coefficient de réflexion spéculaire - grâce à cela, nous utilisons un tampon de moins. Si votre implémentation du rendu différé devient plus complexe et utilise plus de données, vous pouvez facilement trouver de nouvelles façons de combiner les données et de les organiser en textures.


À l'avenir, nous devons rendre les données dans le G-buffer. Si chaque objet a un coefficient de réflexion de couleur, normal et spéculaire, nous pouvons écrire quelque chose comme le shader suivant:


 #version 330 core layout (location = 0) out vec3 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; in vec2 TexCoords; in vec3 FragPos; in vec3 Normal; uniform sampler2D texture_diffuse1; uniform sampler2D texture_specular1; void main() { //       G- gPosition = FragPos; //          G- gNormal = normalize(Normal); //   gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb; //       gAlbedoSpec.a = texture(texture_specular1, TexCoords).r; } 

Puisque nous utilisons plusieurs objectifs de rendu, à l'aide de la layout ce que et dans quel tampon du framebuffer actuel nous rendons. Veuillez noter que nous ne stockons pas le coefficient de miroir dans un tampon séparé, car nous pouvons stocker la valeur flottante dans le canal alpha de l'un des tampons.


Gardez à l'esprit que lors du calcul de l'éclairage, il est extrêmement important de stocker toutes les variables dans le même espace de coordonnées, dans ce cas, nous stockons (et effectuons des calculs) dans l'espace du monde.

Si nous rendons maintenant plusieurs nanosuits dans un tampon G et dessinons son contenu en projetant chaque tampon sur un quart de l'écran, nous verrons quelque chose comme ceci:


img4


Essayez de visualiser la position et les vecteurs normaux et assurez-vous qu'ils sont corrects. Par exemple, les vecteurs normaux pointant vers la droite seront rouges. De même pour les objets situés à droite du centre de la scène. Une fois que vous êtes satisfait du contenu du G-buffer, passons à la partie suivante: le passage de l'éclairage.


Passage d'éclairage


Maintenant que nous avons une grande quantité d'informations dans le G-buffer, nous sommes en mesure de calculer entièrement l'éclairage et les couleurs finales pour chaque pixel du G-buffer, en utilisant son contenu comme entrée pour les algorithmes de calcul d'éclairage. Étant donné que les valeurs du tampon G ne représentent que des fragments visibles, nous effectuerons des calculs d'éclairage complexes exactement une fois pour chaque pixel. Pour cette raison, l'éclairage différé est assez efficace, en particulier dans les scènes complexes, dans lesquelles, lors du rendu direct pour chaque pixel, il est souvent nécessaire de calculer l'éclairage plusieurs fois.


Pour le passage de l'éclairage, nous allons rendre un rectangle plein écran (un peu comme l'effet de post-traitement) et effectuer un calcul lent de l'éclairage pour chaque pixel.


 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gPosition); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, gNormal); glActiveTexture(GL_TEXTURE2); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); //         shaderLightingPass.use(); SendAllLightUniformsToShader(shaderLightingPass); shaderLightingPass.setVec3("viewPos", camera.Position); RenderQuad(); 

Nous lions toutes les textures G-buffer nécessaires avant le rendu et, en outre, définissons les valeurs des variables liées à l'éclairage dans le shader.


Le shader de passage de fragment est très similaire à celui que nous avons utilisé dans les leçons de réunion. Fondamentalement nouvelle est la façon dont nous obtenons une entrée pour l'éclairage directement à partir du G-buffer.


 #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D gPosition; uniform sampler2D gNormal; uniform sampler2D gAlbedoSpec; struct Light { vec3 Position; vec3 Color; }; const int NR_LIGHTS = 32; uniform Light lights[NR_LIGHTS]; uniform vec3 viewPos; void main() { //    G- vec3 FragPos = texture(gPosition, TexCoords).rgb; vec3 Normal = texture(gNormal, TexCoords).rgb; vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb; float Specular = texture(gAlbedoSpec, TexCoords).a; //     vec3 lighting = Albedo * 0.1; //    vec3 viewDir = normalize(viewPos - FragPos); for(int i = 0; i < NR_LIGHTS; ++i) { //   vec3 lightDir = normalize(lights[i].Position - FragPos); vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * lights[i].Color; lighting += diffuse; } FragColor = vec4(lighting, 1.0); } 

Le shader d'éclairage accepte 3 textures qui contiennent toutes les informations enregistrées dans le passage géométrique et dont se compose le G-buffer. Si nous prenons l'entrée pour l'éclairage des textures, nous obtenons exactement les mêmes valeurs que si avec un rendu direct normal. Au début du fragment shader, nous obtenons les valeurs liées aux variables d'éclairage en lisant simplement la texture. Notez que nous obtenons à la fois la couleur et le coefficient de réflexion spéculaire d'une texture - gAlbedoSpec .


Étant donné que pour chaque fragment, il existe des valeurs (ainsi que des variables de shader uniformes) nécessaires pour calculer l'éclairage selon le modèle de Blinn-Fong, nous n'avons pas besoin de changer le code de calcul d'éclairage. La seule chose qui a été modifiée est la façon d'obtenir les valeurs d'entrée.


Le démarrage d'une démo simple avec 32 petites sources lumineuses ressemble à ceci:


img5


L'un des inconvénients de l'éclairage différé est l'impossibilité de mélanger, car tous les tampons g pour chaque pixel contiennent des informations sur une seule surface, tandis que le mélange utilise des combinaisons de plusieurs fragments. (Mélange) , traduction . Un autre inconvénient de l'éclairage différé est qu'il vous oblige à utiliser une méthode commune pour calculer l'éclairage pour tous les objets; bien que cette limitation puisse en quelque sorte être contournée en ajoutant des informations matérielles au g-buffer.


Pour faire face à ces lacunes (notamment le manque de mixage), ils divisent souvent le rendu en deux parties: le rendu avec éclairage différé et la deuxième partie avec un rendu direct destiné à appliquer quelque chose à la scène ou à utiliser des shaders qui ne sont pas compatibles avec un éclairage différé. (Remarque à partir d'exemples: ajout de fumée translucide, de feu, de verre) Pour illustrer le travail, nous allons dessiner les sources lumineuses sous forme de petits cubes en utilisant le rendu direct, car les cubes d'éclairage nécessitent un shader spécial (ils brillent uniformément de la même couleur).


Combinez le rendu différé avec direct.


Supposons que nous voulons dessiner chaque source de lumière sous la forme d'un cube 3D avec un centre coïncidant avec la position de la source de lumière et émettant de la lumière avec la couleur de la source. La première idée qui vient à l'esprit est de rendre directement les cubes pour chaque source lumineuse au-dessus des résultats de rendu différés. Autrement dit, nous dessinons des cubes comme d'habitude, mais uniquement après un rendu différé. Le code ressemblera à ceci:


 //    [...] RenderQuad(); //          shaderLightBox.use(); shaderLightBox.setMat4("projection", projection); shaderLightBox.setMat4("view", view); for (unsigned int i = 0; i < lightPositions.size(); i++) { model = glm::mat4(); model = glm::translate(model, lightPositions[i]); model = glm::scale(model, glm::vec3(0.25f)); shaderLightBox.setMat4("model", model); shaderLightBox.setVec3("lightColor", lightColors[i]); RenderCube(); } 

Ces cubes rendus ne prennent pas en compte les valeurs de profondeur du rendu différé et sont donc toujours dessinés au-dessus des objets déjà rendus: ce n'est pas ce que nous visons.


img6


Nous devons d'abord copier les informations de profondeur du passage géométrique dans le tampon de profondeur, puis seulement dessiner les cubes lumineux. Ainsi, des fragments de cubes lumineux ne seront dessinés que s'ils sont plus proches que des objets déjà dessinés.


Nous pouvons copier le contenu du framebuffer vers un autre framebuffer en utilisant la fonction glBlitFramebuffer . Nous avons déjà utilisé cette fonction dans l'exemple d' anti-aliasing : ( anti-aliasing ), translation . La fonction glBlitFramebuffer copie la partie spécifiée par l'utilisateur du framebuffer dans la partie spécifiée d'un autre framebuffer.


Pour les objets dessinés dans le passage d'éclairage différé, nous avons enregistré la profondeur dans le g-buffer de l'objet framebuffer. Si nous copions simplement le contenu du tampon de profondeur g-buffer dans le tampon de profondeur par défaut, les cubes lumineux seront dessinés comme si toute la géométrie de la scène avait été dessinée en utilisant une passe de rendu directe. Comme cela a été brièvement expliqué dans l'exemple d'anticrénelage, nous devons définir des tampons d'image pour la lecture et l'écriture:


 glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); //   - glBlitFramebuffer( 0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST ); glBindFramebuffer(GL_FRAMEBUFFER, 0); //        [...] 

Ici, nous copions l'intégralité du contenu du tampon de profondeur du framebuffer dans le tampon de profondeur par défaut (si nécessaire, vous pouvez copier les tampons de couleur ou le tampon de stensil de la même manière). Si nous rendons maintenant les cubes lumineux, ils seront dessinés comme si la géométrie de la scène était réelle (bien qu'elle soit aussi simple).


img7


Le code source de la démo se trouve ici .


Avec cette approche, nous pouvons facilement combiner le rendu différé avec le rendu direct. C'est excellent, car nous pouvons appliquer des objets de fusion et de dessin qui nécessitent des shaders spéciaux qui ne sont pas applicables au rendu différé.


Plus de sources lumineuses


L'éclairage différé est souvent apprécié pour pouvoir dessiner un grand nombre de sources lumineuses sans diminution significative des performances. Un éclairage retardé ne permet pas à lui seul de dessiner un très grand nombre de sources lumineuses, car il nous reste à calculer la contribution de toutes les sources lumineuses pour chaque pixel. Pour dessiner un grand nombre de sources lumineuses, une très belle optimisation est utilisée, applicable au rendu différé - la zone d'action des sources lumineuses. (volumes légers)


Habituellement, lorsque nous dessinons des fragments dans une scène très éclairée, nous prenons en compte la contribution de chaque source lumineuse sur la scène, quelle que soit sa distance au fragment. Si la plupart des sources de lumière n'affecteront jamais le fragment, pourquoi perdons-nous du temps à les calculer?


L'idée de l'étendue de la source de lumière est de trouver le rayon (ou le volume) de la source de lumière - c'est-à-dire la zone dans laquelle la lumière peut atteindre la surface. Étant donné que la plupart des sources de lumière utilisent une sorte d'atténuation, nous pouvons trouver la distance maximale (rayon) que la lumière peut atteindre. Après cela, nous effectuons des calculs d'éclairage complexes uniquement pour les sources de lumière qui affectent ce fragment. Cela nous évite un grand nombre de calculs, car nous ne calculons l'éclairage que là où il est nécessaire.


Avec cette approche, l'astuce principale est de déterminer la taille de la zone d'action de la source lumineuse.


Calcul de la portée d'une source lumineuse (rayon)


Pour obtenir le rayon de la source de lumière, nous devons résoudre l'équation d'amortissement de la luminosité, que nous considérons sombre - elle peut être 0,0 ou quelque chose d'un peu plus éclairée, mais toujours sombre: par exemple, 0,03. Pour montrer comment calculer le rayon, nous utiliserons l'une des fonctions d'atténuation les plus complexes et les plus courantes de l' exemple du lanceur de lumière


F l i g h t = f r a c I K c + K ld + K qd 2 


Nous voulons résoudre cette équation pour le cas où F l i g h t = 0 , 0 , c'est-à-dire lorsque la source de lumière est complètement sombre. Cependant, cette équation n'atteindra jamais la valeur exacte de 0,0, il n'y a donc pas de solution. Cependant, nous pouvons à la place résoudre l'équation de la luminosité pour une valeur proche de 0,0, qui peut être considérée comme pratiquement sombre. Dans cet exemple, nous considérons comme acceptable la valeur de luminosité  f r a c 5 256 - divisé par 256, car le framebuffer 8 bits peut contenir 256 valeurs de luminosité différentes.


La fonction d'atténuation sélectionnée devient presque sombre à une distance de gamme, si nous la limitons à une luminosité inférieure à 5/256, alors la gamme de la source de lumière deviendra trop grande - ce n'est pas si efficace. Idéalement, une personne ne devrait pas voir une bordure soudaine et nette de lumière provenant d'une source lumineuse. Bien sûr, cela dépend du type de scène, une valeur plus élevée de la luminosité minimale donne de plus petites zones d'action des sources de lumière et augmente l'efficacité des calculs, mais peut conduire à des artefacts perceptibles dans l'image: l'éclairage se brisera brusquement aux frontières de la zone d'action de la source de lumière.

L'équation d'atténuation que nous devons résoudre devient:


 frac5256= fracImaxAtténuation

é


Ici Imax - la composante la plus brillante de la lumière (provenant des canaux r, g, b). Nous utiliserons le composant le plus brillant, car les autres composants donneront une restriction plus faible sur la portée de la source lumineuse.


Nous continuons de résoudre l'équation:


 frac5256 cdotAttenuation=Imax


Attenuation=Imax cdot frac2565


Kc+Kl cdotd+Kq cdotd2=Imax cdot frac2565


Kc+Kl cdotd+Kq cdotd2Imax cdot frac2565=0


La dernière équation est une équation quadratique sous la forme ax2+bx+c=0 avec la solution suivante:


x= fracKl+ sqrtK2l4Kq(KcImax frac2565)2Kq


Nous avons obtenu une équation générale qui nous permet de substituer les paramètres (atténuation constante, coefficients linéaires et quadratiques) pour trouver x - le rayon de la source lumineuse.


 float constant = 1.0; float linear = 0.7; float quadratic = 1.8; float lightMax = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b); float radius = (-linear + std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax))) / (2 * quadratic); 

La formule renvoie un rayon compris entre environ 1,0 et 5,0 en fonction de la luminosité maximale de la source lumineuse.


On retrouve ce rayon pour chaque source lumineuse sur la scène et on l'utilise afin de ne prendre en compte que les sources lumineuses dans lesquelles il se situe dans le cadre de chaque fragment. Ci-dessous, un passage refait de l'éclairage qui prend en compte les zones d'action des sources lumineuses. Veuillez noter que cette approche est mise en œuvre uniquement à des fins éducatives et ne convient pas à une utilisation pratique (nous expliquerons bientôt pourquoi).


 struct Light { [...] float Radius; }; void main() { [...] for(int i = 0; i < NR_LIGHTS; ++i) { //         float distance = length(lights[i].Position - FragPos); if(distance < lights[i].Radius) { //     [...] } } } 

Le résultat est exactement le même qu'avant, mais maintenant pour chaque source lumineuse son effet n'est pris en compte que dans la zone de son action.


Le code final est une démo. .


L'application réelle de la portée de la source lumineuse.


Le fragment shader montré ci-dessus ne fonctionnera pas en pratique et ne sert qu'à illustrer comment nous pouvons nous débarrasser des calculs d'éclairage inutiles. En réalité, la carte vidéo et le langage de shader GLSL optimisent très mal les boucles et les branches. La raison en est que l'exécution du shader sur la carte vidéo est effectuée en parallèle pour différents pixels, et de nombreuses architectures imposent la limitation qu'en exécution parallèle, différents threads doivent calculer le même shader. Cela conduit souvent au fait que le shader en cours d'exécution calcule toujours toutes les branches de sorte que tous les shaders fonctionnent en même temps. (Notez la piste. Cela n'affecte pas le résultat des calculs, mais cela peut réduire les performances du shader.) Pour cette raison, il peut s'avérer que notre contrôle de rayon est inutile: nous calculerons toujours l'éclairage pour toutes les sources!


Une approche appropriée pour utiliser la portée de la lumière consiste à rendre des sphères avec un rayon semblable à celui d'une source de lumière. Le centre de la sphère coïncide avec la position de la source lumineuse, de sorte que la sphère contient en elle-même la plage d'action de la source lumineuse. Il y a une petite astuce ici - nous utilisons essentiellement le même shader de fragment différé pour dessiner une sphère. Lorsque vous dessinez une sphère, le fragment shader est appelé spécifiquement pour les pixels affectés par la source de lumière, nous ne rendons que les pixels nécessaires et sautons tous les autres. :


img8


, . , , . _*__
_ + __ , .


: ( ) , , , - ( ). stenil .


, , , . ( ) : c (deferred lighting) (tile-based deferred shading) . MSAA. .


vs


( ) - , , . , — , MSAA, .


( ), ( g- ..) . , .


: , , , . , , , . . parallax mapping, , . , .


Liens annexes



PS - . , !

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


All Articles