Les termes HDR et bloom sont beaucoup utilisés depuis quelques temps, souvent avec une certaine confusion. Le terme HDR (high dynamic range) arrive avant tout du monde de la photo (on parle souvent de HDRI dans ce domaine). Le principe est simple : stocker plus d’informations que ce qu’une image contient en général.
Wikipedia offre un exemple très parlant :
Une même scène photographiée avec différents temps d’ouvertures (de 1/40 seconde à 25 secondes) donne des résultats très différents : l’idée est donc de conserver non pas une image, mais l’ensemble de ces prises de vue au sein d’un même fichier, par exemple en attribuant plus de place (16 ou 32 bits par composante à la place de des 8 bits habituels), et/ou en stockant des informations plus proche de la "réalité" (luminance et radiance/radiosité) en lieu et place ou en complément d’informations purement "numériques" (taux rouge/vert/bleu).
Il devient donc possible de conserver l’ensemble des détails de la scène alors que le contraste de l’image est très fort :
Cette image classique (RGB, 8 bits par couleur) et issue de la "somme" des couches de l’image HDR. Il est donc tout à fait possible de jouer ultérieurement sur l’image originale pour refaire sortir les nuances sombres ou claires, par exemple. Le résultat est tout à fait intéressant, puisqu’il permet de souligner les zones très lumineuses (grâce au halo qui se forme autour de ces zone) sans perdre des détails des zones plus sombres. Une scène 3D peut donc gagner en réalisme en générant ce genre d’effet.
Dès lors, comment obtenir ce genre de rendu avec des images générées par une carte graphique ?
Comme souvent pour ce genre d’effets, différentes méthodes existent. L’une d’elles, probablement la plus commune, consiste à pousser la "précision" du rendu.
Pour prendre l’exemple d’OpenGL, toutes les couleurs s’expriment sur la fourchette [0,1] : pour créer du jaune, je vais réaliser quelque chose du genre glColor3f(1.0,0.7,0.1);. Toute valeur qui dépasse l’une des bornes de cette fourchette est "clampée" à la borne. Un glColor3f(42,42,42); ne va pas exploser mon écran mais générer … du blanc (1,1,1).
Eh bien si nous sommes capables d’intercepter ces valeurs avant un éventuel "clamp", il devient possible de faire des choses intéressantes : imaginez une surface rouge claire (0.7,0,0). Si une lumiére "blanche" (0.7,0.7,0.7) vient à éclairer directement cette surface, le résultat va être (1.4,0.7,0.7). En théorie, OpenGL doit clamper ce résultat à (1.0,0.7,0.7), que l’on va qualifier de "rouge très brillant" (c’est une monstrueuse simplification du fonctionnement d’un pipeline de rendu, mais l’idée est là).
Avec un shader (programme destiné à être executé dans la carte vidéo et non sur le CPU), il est possible de placer un morceau de code avant le clamp pour détecter toutes les valeurs supérieures à 1 et les conserver. On stocke toutes ces valeurs dans une texture à coté et on laisse OpenGL faire son clamp. Une fois l’image terminée, on retrouve à l’écran la partie "basse" de l’image, celle qui contient les couleurs habituelles. La texture, elle, contient la partie "HDR" de l’image. Appliquez un très fort effet de flou sur cette dernière, et recopiez la (blend) au dessus de l’image à l’écran : toutes les couleurs "hautes" bavent :
Réaliser tout ceci directement dans le GPU demande pas mal de capacités à la carte vidéo, ainsi qu’un certain nombre de calculs : interception, stockage, blur, etc.
C’est là que le bloom arrive : il permet de réaliser le même genre d’effet d’éblouissement et/ou de zone lumineuses "baveuses" que le HDR, mais pour un cout moindre. Il suffit ici d’appliquer l’effet de blur à l’image ou à une partie de l’image (les lumières seulement, par exemple) et de recopier là aussi le tout au dessus de l’image originale. Le problème principal de cette méthode étant l’effet de flou assez fort qui résulte de l’opération : le traitement s’appliquant sur une image non HDR (sur une fourchette [0,1] donc), le rendu est forcément moins précis dans les dégradés de teintes, par exemple.
J’en arrive à Raydium. Ceux qui suivent l’histoire de ce moteur 3D ont probablement remarqué que l’une des contraintes du développement est le besoin de permettre au moteur de tourner sur de "petites" machines. Un Athlon 1 GHz avec une GeForce 2 GTS est par exemple une machine raisonnable pour Raydium. Exit les shaders, les FBO flottants, …
La question était donc de savoir s’il n’était pas possible de réaliser un effet qui va au delà du bloom, mais sans demander les calculs d’un rendu HDR. Ze "pseudo-HSR" is born.
Premier besoin : être capable d’identifier les surfaces "brillantes" sans passer par un shader. Je triche ici en ajoutant un attribut à certaines textures. Par exemple : texture utilisée pour l’ampoule d’une lampe, pour une vitre de photocopieur, la skybox, …
Ensuite, il faut pouvoir rendre l’impression "d’adaptation" de l’oeil à la lumière. L’effet doit par exemple s’estomper rapidement, mais être capable de se reproduire si l’exposition à la lumière se reproduit après un passage dans une zone sombre. Cela implique d’être capable de connaître à tout moment la quantité de lumière reçue par la "caméra".
Pour les premiers tests sur ce sujet, j’utilise le stencil buffer de la carte vidéo. A chaque nouvelle frame, le stencil est rempli de "1" (blanc). Lors du dessin des différents triangles de la scène, ceux qui sont texturés avec une texture dont l’attribut "lumineux" n’existe pas engendrent l’activation de l’écriture dans le stencil, et ce en noir ("0"). (L’inverse est bien sûr possible)
Démonstration étape par étape. Prenons cette scène :
Ici, le ciel (les 6 textures de la skybox) est marqué comme "lumineux". Le stencil ressemble donc à ça :
C’est là que commence le plus gros du boulot. Il faut ici réussir à appliquer un énorme effet de flou à cette image, pour un faire un masque à appliquer au dessus de la scène rendue (première image). Pour faciliter un filtrage fort, il faut bien sûr perdre du détail, et un redimensionnement de l’image va nous y aider. Pour cette étape, ainsi que pour la suivante, la carte vidéo n’est pas compétente (cf. conditions plus haut) et il est donc nécessaire de descendre le stencil buffer de la carte vidéo vers la RAM, pour pouvoir bosser avec le CPU (hop, un peu de bande passante AGP en moins). Avec un "resize au plus proche" (sans filtrage) en 64×64, on arrive à ceci :
(ici zoomé 4 fois)
Et c’est sur cette image qu’on va appliquer l’effet de flou. L’algo est assez trivial, et consiste en gros à faire la moyenne des 8 pixels autour d’un pixel pour en trouver sa nouvelle valeur (on parle de matrice de convolution). Le traitement est ici réalisé sur une petite image (64×64) et en niveaux de gris, ce qui permet une exécution rapide… et tant mieux, puisque pour obtenir un effet intéressant, il faut répéter l’opération plus de dix fois !
L’effet d’escalier est ici uniquement dû au zoom 4x destiné à mieux montrer le résultat. La véritable image est celle là :
Maintenant que l’effet de flou est appliqué, on va pouvoir redonner l’image à la carte vidéo. Il faut donc remonter les 64×64 pixels dans la carte vidéo, en tant que texture alpha ("carte de transparence"). Paradoxalement, il faut noter que pour l’effet de pseudo-HDR, la texture ne va pas être utilisée en alpha, le but étant uniquement ici de de gagner de la place et de la bande passante (en alpha, chaque pixel utilise 8 bits, et non 24 ou 32).
Il faut se souvenir que l’image actuellement affichée à l’écran (dans la back-buffer, en réalité) est la scène "terminée", déjà rendue. L’ensemble des opérations réalisées entre temps se sont passées en dehors du "color framebuffer", et restent donc invisibles pour l’observateur. Il faut donc maintenant exploiter notre nouvelle texture pour modifier l’image rendue (on est vraiment dans de la "post-prod" ici). Pour éviter l’effet d’aliasing que l’on voit dans l’agrandissement de l’image précédente, on va utiliser un méchanisme tout bête de la carte vidéo : le filtrage (bi)linéaire (quasiment identique à l’effet de flou donné plus haut).
Le tout se déroule ainsi :
On configure la carte vidéo dans un mode différent du mode habituel : GL_ADD. Chaque pixel (fragment) qui arrive dans le color frame-buffer (ce que l’on voit à l’écran) ne remplace pas celui qui était là avant lui, mais s’y additionne. Il suffit ensuite de dessiner notre texture de 64×64 étirée "en plein écran" :
Avec le mode GL_ADD, on va donc arriver au résultat "saturé" suivant :
Voilà ou en sont mes tests pour l’instant, il reste encore pas mal de choses à réaliser pour en faire un effet intéressant et pas trop envahissant (cf. atténuation avec le temps dont je parle plus haut), mais le principe semble marcher. L’impact sur le framerate est sensible, principalement à cause du coût de la descente du stencil vers la RAM dans les grandes résolutions (1 Mo par frame sur ma machine de test). Je pense pouvoir simplement contourner ce problème en lisant les 64×64 pixels un par un depuis la carte.
En éspérant avoir été didactique … à suivre !
Ref : The Making Of "Shadow Of The Colossus" pour sa section qui parle du pseudo-HDR sur la PS2. Pour la petite info, pour cet effet de pseudo-HDR, la PS2 possède un (très) gros avantage sur le PC : sa mémoire est unifiée, ce qui évite les échanges entre la mémoire de la carte vidéo et la RAM par le bus AGP/PCI-E comme c’est le cas avec l’algo présenté ici.
Laisser un commentaire
Vous devez vous connecter pour publier un commentaire.