Navigation▲
Tutoriel précédent : unités de texture |
Tutoriel suivant : Early-Z |
I. Introduction▲
Pour rappel, nos fragments ne sont pas tout à fait des pixels. Il s'agit de données qui vont permettre, une fois combinées, d'obtenir la couleur finale d'un pixel. Ces fragments contiennent diverses informations :
- position à l'écran ;
- profondeur ;
- couleur ;
- valeur de stencil ;
- transparence alpha.
Une fois que nos fragments se sont vu appliquer une texture, il faut les enregistrer dans la mémoire, afin de les afficher. On pourrait croire qu'il s'agit là d'une opération très simple, mais ce n'est pas le cas. Il reste encore un grand nombre d'opérations à effectuer sur nos pixels : la profondeur des fragments doit être gérée, de même que la transparence, etc.
Ces opérations sont les suivantes :
- la gestion des profondeurs des fragments ;
- les mélanges de couleurs et la gestion de la transparence ;
- l'anticrénelage ;
- et enfin la gestion du stencil buffer.
Elles sont réalisées dans un circuit qu'on nomme le Render Output Target. Celui-ci est le tout dernier circuit, celui qui enregistre l'image finale dans la mémoire vidéo. Ce qui suit va aborder ce circuit dans les grandes lignes.
II. Test de visibilité▲
Pour commencer, il va falloir trier les fragments par leur profondeur. Je vous rappelle que ces fragments viennent de la rasterisation d'un triangle sur les pixels de l'écran. Ces triangles sont placés dans une scène 3D, alors que l'écran est un plan 2D. En conséquence, du point de vue de l'écran, il arrive qu'un triangle en cache un autre.
Prenons deux objets, deux murs par exemple : un rouge et un bleu. Dans ce cas, un pixel de l'écran sera associé à deux fragments : un pour le mur rouge et un pour le bleu. Il est évident que les pixels associés au mur doivent avoir la couleur du mur qui est devant. De tous les fragments, un seul doit être choisi : celui qui est devant. Reste à faire ce choix.
Pour cela, la coordonnée de profondeur est calculée durant l'étape de la rasterisation. Cette coordonnée de profondeur indique la profondeur d'un fragment dans le champ de vision. Cette coordonnée est souvent appelée la coordonnée z. Ainsi, le choix entre deux fragments à afficher est simple : on prend celui dont la coordonnée z indique qu'il est le plus proche. Par convention, plus la coordonnée z est petite, plus l'objet est près de l'écran.
Petite précision : il est assez rare qu'un objet soit caché seulement par un seul objet. En moyenne, un objet est caché par trois à quatre objets dans un rendu 3D de jeu vidéo.
II-A. Z-buffer▲
Pour savoir quels fragments sont à éliminer (car cachés par d'autres), notre carte graphique va ruser. Les différents fragments d'un pixel ne sont pas disponibles immédiatement et les comparer les uns avec les autres n'est pas possible. Dans une scène 3D, les objets sont rendus un par un et savoir quand un objet en masque un autre sera rendu est impossible.
Pour éviter tout problème, notre carte graphique va utiliser ce qu'on appelle un tampon de profondeur (depth-buffer). Il s'agit simplement d'un tableau, stocké en mémoire vidéo. Ce tableau va stocker, pour chaque pixel, la coordonnée z de l'objet le plus proche déjà rendu.
Au fur et à mesure que les objets seront calculés, leurs fragments seront envoyés au circuit de gestion de la visibilité. Celui-ci va alors récupérer la coordonnée z du fragment reçu et la comparer à la coordonnée z stockée dans ce tampon de profondeur. Si jamais la valeur la plus petite est celle du tampon de profondeur, alors le fragment rendu auparavant est plus près : on abandonne le fragment tout juste reçu. Dans le cas contraire, le fragment reçu est plus près. Il va être écrit en mémoire, et surtout : sa coordonnée z va remplacer l'ancienne valeur z dans le tampon de profondeur.
Par défaut, ce tampon de profondeur est rempli avec la valeur de profondeur maximale.
Cette coordonnée z est un nombre codé sur plusieurs bits. Les premières cartes graphiques utilisaient des nombres codés sur 16 bits. Utiliser moins, comme 8 bits, était impossible : le tampon de profondeur n'est pas assez précis. Si deux objets sont suffisamment proches, le tampon de profondeur n'aura pas la précision suffisante pour discriminer les deux objets : pour lui, les deux objets seront à la même place. Conséquence : il faut bien choisir un des deux objets. Si l'objet choisi est le mauvais, des artefacts visuels apparaissent. Voici ce que cela donne :
Autant le dire tout de suite : 16 bits est suffisant, mais des artefacts visuels apparaissent de temps à autre. C'est ce qui fait que de nos jours, des tampons de profondeur plus précis sont utilisés : la coordonnée z est codée sur 24, voire 32 bits.
On peut préciser qu'il existe des variantes du tampon de profondeur, qui utilisent un codage de la coordonnée de profondeur assez différent. On peut notamment citer :
- le tampon de profondeur irrégulier (Irregular Z-buffer) ;
- le tampon W (W-buffer).
Ils se distinguent du tampon de profondeur par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Avec eux, la précision est meilleure pour les fragments proches de la caméra, et plus faible pour les fragments éloignés. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence.
II-B. Circuit de gestion de la profondeur▲
Notre profondeur est gérée par un circuit spécialisé. Celui-ci va devoir :
- récupérer les coordonnées du fragment reçu à l'écran ;
- lire en mémoire la coordonnée z correspondante dans le tampon de profondeur ;
- comparer celle-ci avec la coordonnée z du fragment reçu ;
- et décider s'il faut mettre à jour le tampon d'image et le tampon de profondeur.
Comme vous le voyez, ce circuit va devoir effectuer des lectures et des écritures en mémoire vidéo. Autant prévenir tout de suite : ces lectures et écritures utilisent fortement la mémoire. Et celle-ci est déjà mise à rude épreuve avec les chargements de sommets et les chargements de textures. Diverses techniques existent pour limiter l'utilisation de la mémoire. Ces optimisations cherchent à diminuer :
- la quantité de mémoire vidéo utilisée ;
- le nombre de lectures et écritures dans celle-ci.
II-B-1. Z Compression▲
Une première solution consiste à compresser le tampon de profondeur dans la mémoire vidéo. Si un ROP veut lire la coordonnée z, il va devoir lire un morceau du tampon de profondeur, le décompresser et l'utiliser dans ses calculs. Évidemment, les données devront être compressées avant d'être stockées dans le tampon de profondeur.
Cette compression est une compression sans perte. Pour donner un exemple, nous allons prendre la z-compression des cartes graphiques ATI Radeon 9800. Cette technique de compression découpait des morceaux de 8 * 8 fragments et les encodait avec un algorithme nommé DDPCM : Differential Differential Pulse Code Modulation.
Ce découpage du tampon de profondeur en morceaux carrés est souvent utilisé dans la majorité des circuits de compression et de décompression de la profondeur. Toutefois, il arrive que certains de ces blocs ne soient pas compressés : certains blocs sont compressés, d'autres non. Tout dépend de la compression, si elle permet de gagner de la place ou pas : c'est le circuit de compression qui décide s'il faut compresser un bloc ou non. On trouve un bit au tout début de ce bloc qui indique s'il est compressé ou non.
II-B-2. Fast Z Clear▲
Entre deux images, le tampon de profondeur doit être remis à zéro. La technique la moins performante consiste à réécrire tout son contenu avec la valeur maximale. Cela peut prendre pas mal de temps.
Pour éviter cela, chaque bloc contient un bit, qui indique si le bloc en question est remis à zéro. Si ce bit est positionné à 1, alors le ROP va faire comme si le bloc avait été remis à zéro. Et cela même si ce n'est pas le cas. Ainsi, au lieu de réécrire tout le bloc, il suffit de réécrire un bit par bloc. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant.
II-B-3. Z-cache▲
Une dernière optimisation possible consiste à ajouter une mémoire cache qui stocke les derniers blocs de coordonnées z lues ou écrites depuis la mémoire. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes et on le conserve pour gérer les fragments qui suivent.
III. Transparence▲
Bien, la profondeur est une chose de réglée. Du moins, pour le moment. Il nous reste à traiter la transparence. Dans nos rendus 3D et nos jeux vidéo, il arrive que certaines textures soient transparentes. En clair : on voit à travers. Cette transparence est gérée comme une sorte de couleur ajoutée aux composantes RGB : elle est codée par un nombre, le canal alpha, qui indique si un pixel est plus ou moins transparent.
Et là, c'est le drame : que se passe-t-il si un fragment transparent est placé devant un autre fragment ? Je vous le donne en mille : le tampon de profondeur tel qu'on l'a vu fonctionnera à la perfection. Malheureusement, la couleur du pixel ne sera pas celle du pixel qui cache l'autre. Celui-ci est transparent et laisse donc passer la couleur du fragment caché. Comment calculer la couleur finale du pixel à partir de fragments contenant de la transparence ? Sur le principe, la couleur sera un mélange de la couleur du fragment transparent et de la couleur du (ou des) fragment(s) placé(s) derrière. Mais comment effectuer ce calcul ?
III-A. Alpha Blending▲
Ce calcul va nous servir à ajuster la couleur du pixel final de notre image : la couleur à afficher à l'écran, quoi… Pour chaque pixel, on aura donc une couleur finale. Notre carte graphique contient un tampon de couleur, une portion de la mémoire vidéo qui va servir à stocker, pour chaque pixel, sa couleur finale.
Calculer la couleur finale s'effectue simplement : à chaque fragment envoyé dans le ROP, celui-ci va :
- lire l'ancienne couleur contenue dans le tampon de couleur ;
- calculer la couleur finale en fonction de la couleur du fragment qui est devant et de celle lue depuis le tampon de couleur ;
- et enregistrer le résultat.
Le calcul à effectuer est très simple. Première précision : A est la couleur de l'élément qui est devant, B est la couleur de l'élément qui est derrière. a et b sont les valeurs alpha respectives de A et B. Le calcul de la couleur finale s'effectue pour chaque composante R, G, B et alpha.
La valeur de transparence finale se calcule avec cette formule : a_0 = a + b * (1 - a)
Les composantes R, G et B se calculent comme suit :
kitxmlcodelatexdvpC = \frac{( A * a ) + ( B * b * (1 - a)}{a_0}finkitxmlcodelatexdvpIII-B. Alpha Test▲
Certaines vieilles cartes graphiques possédaient une « optimisation » assez intéressante : l'alpha test. Cette technique consistait à ne pas enregistrer en mémoire les fragments dont la couleur alpha était inférieure à un certain seuil. De nos jours, cette technologie est devenue obsolète.
III-C. Color ROP▲
Ces opérations de test et de fondu sont effectuées par un circuit spécialisé : le ROP de couleur. Celui-ci travaille en parallèle du ROP de profondeur. Il va ainsi mélanger et tester nos couleurs pendant que le ROP de profondeur effectue ses comparaisons entre coordonnées z.
Et comme toujours, les lectures et écritures de couleurs vont utiliser de la mémoire vidéo, et encore une fois, on va devoir trouver des optimisations pour limiter le casse. Première optimisation : ajouter une mémoire cache qui stocke les derniers blocs de couleur lus ou écrits depuis la mémoire. Autre optimisation : compresser les couleurs. Cela a une tête de déjà-vu.
Il est à noter que sur certaines cartes graphiques, l'unité en charge de calculer les couleurs peut aussi servir à effectuer des comparaisons de profondeur. Ainsi, si jamais on tombe uniquement sur des fragments opaques, l'unité de traitement de la transparence peut servir à autre chose au lieu de se tourner les pouces. Il est alors possible de traiter les coordonnées de deux fragments à la fois : un, dans l'unité de gestion de la profondeur et un autre, dans l'unité de gestion des couleurs. C'est notamment le cas dans la carte graphique Geforce FX de Nvidia. Ce genre d'optimisation est très utile dans certains jeux vidéo. C'est notamment le cas dans DOOM 3.
IV. Anticrénelage▲
Notre circuit de ROP est enfin chargé d'une dernière tâche : l'anticrénelage. L' anticrénelage est une technologie qui permet d'adoucir les bords des objets. Le fait est que dans les jeux vidéo, les bords des objets sont souvent pixelisés, ce qui leur donne un effet d'escalier.
Prenons par exemple cette image :
À gauche, la version sans anticrénelage. À droite, la version avec.
Si vous regardez cette image de loin, vous aurez tendance à croire qu'il s'agit d'une lettre moins pixelisée que précédemment. Le fait est que le filtre d'anticrénelage a rajouté une sorte de dégradé pour adoucir les bords de notre ligne, rendant celle-ci plus continue, plus lisse, moins pixelisée.
IV-A. Types d'anticrénelage▲
Il existe un grand nombre de techniques d'anticrénelage (antialiasing) différentes. Toutes ont des avantages et des inconvénients en termes de performances ou de qualité d'image. Dans ce qui va suivre, nous allons voir ces différentes techniques.
IV-A-1. SSAA - SuperSampling AntiAliasing▲
La première technique consiste simplement à calculer l'image a une résolution supérieure et à la réduire avant de l'afficher. Par exemple, si je veux afficher une image en 1280 * 1024, la carte graphique va calculer une image en 2560 * 2048, avant de la réduire. On appelle cet antialiasing le Supersampling antialiasing ou SSAA.
Si vous regardez les options de vos pilotes de carte graphique, vous verrez qu'il existe plusieurs réglages pour l'anticrénelage : 2X, 4X, 8X, etc. Cette option signifie que l'image calculée par la carte graphique contiendra respectivement 2, 4 ou 8 fois plus de pixels que l'image originale.
Pour effectuer la réduction de cette image 2, 4 ou 8 fois plus grande, notre ROP va découper l'image en blocs de 2, 4 ou 8 pixels, et va effectuer un « mélange » des couleurs de tout le bloc. Ce « mélange » est en réalité une série d'interpolations linéaires : repensez au chapitre sur le filtrage des textures : ici, c'est la même chose, mais avec des couleurs de fragments. Cette série d'interpolations linéaires permettra donc de calculer la couleur finale du pixel.
Niveau avantage, cette technique filtre toute l'image. Elle permet ainsi de filtrer les bords situés à l'intérieur des textures, et notamment, des textures contenant des parties transparentes.
Niveau désavantage, le SSAA va augmenter la résolution des images à traiter. Ce qui signifie une augmentation de la consommation de la mémoire vidéo (tampon d'image, tampon de couleur, tampon de profondeur, etc.), du temps de calcul (on calcule quatre fois plus de pixels) et d'autres désagréments.
IV-A-2. MSAA - MultiSampling AntiAliasing▲
Pour réduire la consommation de mémoire induite par le SSAA, il est possible d'améliorer celui-ci pour faire en sorte qu'il ne filtre pas toute l'image, mais seulement les bords des objets. Après tout, les bords des objets sont censés être les seuls endroits où l'effet d'escalier est censé se faire sentir, alors pourquoi filtrer le reste ? Cette optimisation a donné naissance au MultiSampling AntiAliasing, abrégé en MSAA.
Comme je l'ai dit, cette technique est une optimisation du SSAA : l'image à afficher est rendue dans une résolution supérieure. Les unités de traitement de fragments ont donc besoin de traiter des images 4, 9, 16, 25, etc., fois plus grosses. Ces images contiendront 4, 9, 16, 25, etc., fois plus de fragments. Ces fragments sont regroupés en blocs de 4, 9, 16, etc. : chacun de ces blocs correspondra à un pixel à afficher à l'écran. Pour nous faciliter la tâche, nous allons appeler les fragments d'un même bloc des sous-pixels.
Seulement, il y a une différence : les textures ne s'appliquent pas aux fragments individuels, mais à tout un bloc de fragments. Lorsque l'on voudra appliquer une texture à un pixel, la couleur calculée grâce au filtrage sera appliquée de manière identique pour chaque sous-pixels : ils auront tous la même couleur. Avec le SSAA, chaque sous-pixel se verrait appliquer un morceau de texture indépendamment des autres. Ce qui fait que le filtrage de texture pourrait leur donner des couleurs différentes.
Le MSAA va jouer sur l'enregistrement de cette couleur dans le tampon de couleur. Le tampon de couleur contiendra une couleur pour chaque sous-pixel. Cette couleur dépendra de la position du sous-pixel : est-il dans le triangle qui lui a donné naissance (à l'étape de rasterisation), ou en dehors du triangle. Si le sous-pixel est complètement dans le triangle, sa couleur sera celle de la texture appliquée au bloc de sous-pixels. Si le sous-pixel est en dehors du triangle, on placera un zéro dans le tampon de couleur.
Pour obtenir la couleur finale du pixel à afficher, le ROP va prendre les couleurs des sous-pixels adéquats, va les mélanger (série d'interpolations linéaires, comme dit plus haut), et va enregistrer la couleur finale obtenue dans le tampon d'image.
Niveau avantages, il faut remarquer que le MSAA n'utilise qu'un seul filtrage de textures pour chaque pixel final. Le SSAA aurait effectué un filtrage par sous-pixel. Le MSAA demande donc d'effectuer moins de calculs dans les unités de traitement de fragments et de textures, et économise de la bande-passante mémoire.
Niveau désavantage, il faut remarquer que le MSAA ne filtre que les bords des objets géométriques. Il ne filtre pas l'intérieur des textures. On pourrait penser que ce n'est pas un problème, mais il existe une exception : les textures transparentes. Il arrive que certains contours d'objets soient définis avec des textures transparentes. Prenons un arbre : ses feuilles ne sont pas modélisées avec des triangles ou des objets géométriques complexes. À la place, chaque feuille est attribuée à un rectangle, sur lequel on place une image de feuille. La feuille est définie par une texture dont une partie est transparente : ce qui n'appartient pas à la feuille. Avec le MSAA, les bords d'une telle feuille ne seront pas filtrés.
Pour résoudre ce problème, les fabricants de cartes graphiques ont créé diverses techniques pour appliquer l'anticrénelage à l'intérieur des textures alpha. Les détails de ces techniques ne sont, à ma connaissance, pas encore connus.
IV-A-3. FAA : Fragment AntiAliasing▲
Comme on l'a vu, le MSAA utilise une plus grande quantité de mémoire vidéo. La quantité de lectures et d'écritures augmente en proportion.
Le Fragment AntiAliasing, ou FAA, cherche à diminuer la quantité de mémoire vidéo utilisée par le MSAA. Le FAA fonctionne sur le même principe que le MSAA, à un détail près : il ne stocke pas les couleurs pour chaque sous-pixel, mais utilise à la place un masque.
Pour chaque bloc, le MSAA stockera quatre couleurs : une par sous-pixels. Ces sous-pixels peuvent avoir seulement deux couleurs : soit ils ont la couleur calculée lors du filtrage de textures, soit ils ont une couleur noire (par défaut).
Le FAA stockera une couleur, et un petit groupe de quelques bits. Chacun de ces bits sera associé à un des sous-pixels du bloc et indiquera sa couleur :
- 0 si le sous-pixel a la couleur noire (par défaut) ;
- et 1 si la couleur est à lire depuis le tampon de couleur.
Le ROP utilisera ce masque pour déterminer la couleur du sous-pixel correspondant.
Avec le FAA, la quantité de mémoire vidéo utilisée est fortement réduite. Et la quantité de données à lire ou écrire pour effectuer l'anticrénelage diminue aussi fortement. Cela libère la mémoire vidéo pour autre chose : lectures ou écritures dans des textures, chargement de sommets, etc. Mais le FAA a un défaut : il se comporte assez mal sur certains objets géométriques, donnant naissance à des artefacts visuels.
Il existe d'autres techniques d'anticrénelage, mais on n'en parlera pas pour le moment.
IV-B. Position des échantillons▲
Un point important concernant la qualité de l'anticrénelage concerne la position des sous-pixels sur l'écran. Comme vous l'avez vu dans le chapitre sur la rasterisation, notre écran peut être vu comme une sorte de carré, dans lequel on peut repérer des points. Chacun de ces points peut être repéré par deux nombres flottants. Nos pixels sont des points, placés sur cet écran, et leur couleur est interpolée en fonction de leur position par rapport aux sommets.
Reste que l'on peut placer ces pixels n'importe où sur l'écran, et pas forcément à des positions que les pixels occupent exactement sur l'écran. Pour des pixels, il n'y a aucun intérêt à faire cela, sinon à dégrader l'image. Mais pour des sous-pixels, cela change. Toute la problématique peut se résumer en une phrase : où placer nos sous-pixels pour obtenir une meilleure qualité d'image possible.
IV-B-1. Grille simple▲
La solution la plus simple consiste à placer nos sous-pixels à l'endroit qu'ils occuperaient si l'image était réellement rendue avec la résolution simulée par l'anticrénelage.
On peut penser qu'il s'agit là de la meilleure solution. Mais dans la réalité, cette solution a tendance à mal gérer les lignes pentues. Elle fonctionne notamment particulièrement mal pour les bords de surface qui font un angle de 45 degrés par rapport à l'horizontale ou la verticale.
IV-C. Grille tournée▲
Pour mieux gérer les bords penchés, on peut aussi positionner nos sous-pixels comme ceci :
Le principe est simple : nos sous-pixels sont placés sur un carré (ou sur une ligne si l'on dispose seulement de deux sous-pixels). Ce carré peut être obtenu en prenant la grille vue au-dessus (la position des pixels « normale ») : il suffit de diminuer sa taille et de la faire tourner. Des mesures expérimentales montrent que la qualité optimale semble être obtenue avec un angle de rotation de kitxmlcodeinlinelatexdvparctan(1/2)finkitxmlcodeinlinelatexdvp (26,6 degrés), et un facteur de rétrécissement de kitxmlcodeinlinelatexdvp\sqrt{5}/2finkitxmlcodeinlinelatexdvp.
V. Effets de brouillard▲
Le ROP peut aussi ajouter des effets de brouillard dans notre scène 3D. Ce brouillard sera simplement modélisé par une couleur. Cette couleur, la couleur de brouillard, sera alors mélangée avec la couleur du pixel calculée, pour obtenir un effet de brouillard. L'effet de brouillard appliqué sur les pixels est particulièrement simple.
Cette couleur de brouillard peut être calculée de deux façons : par le ROP ou par les unités de traitement des sommets. Les premières versions calculaient une couleur de brouillard associée à chaque sommet. Cette couleur parcourait la carte graphique avec le fragment associé et était mélangée avec la couleur du fragment lors de l'arrivée dans le ROP. Par la suite, cette couleur de brouillard a été calculée dans les ROP directement, en fonction de la coordonnée de profondeur du fragment.
Si jamais l'objet est trop proche, la couleur de brouillard est nulle : il n'y a pas de brouillard. Cette distance d'apparition du brouillard sera nommée fogstart dans ce qui suit. Par contre, au-delà d'une certaine distance, l'objet est intégralement dans le brouillard et seul celui-ci est visible. Cette distance sera nommée fogend dans ce qui suit. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs.
Suivant la distance, cette couleur de brouillard sera affaiblie. Notre carte graphique stocke ainsi une couleur de brouillard de base, sur laquelle elle effectuera un calcul pour déterminer la couleur de brouillard à appliquer au pixel. Par la suite, cette couleur sera mélangée avec la couleur du pixel, par un simple calcul de moyenne.
Le calcul de la couleur de brouillard dans l'intervalle mentionné plus haut peut s'effectuer de diverses façons :
- par un calcul linéaire : kitxmlcodeinlinelatexdvp\frac{fogend - z} {fogendd-fogstart}finkitxmlcodeinlinelatexdvp
- avec une exponentielle : kitxmlcodeinlinelatexdvp\frac{1} {e^(z * fogdensity)}finkitxmlcodeinlinelatexdvp
VI. Stencil buffer▲
En plus du tampon de profondeur et du tampon de couleur, certaines cartes graphiques permettent le support matériel d'un troisième tampon, présent en mémoire vidéo : le stencil buffer. Celui-ci n'a pas vraiment d'utilité bien définie, et peut servir à beaucoup de choses diverses et variées. Dans la majorité des cas, celui-ci sert pour effectuer des calculs d'éclairage. D'ordinaire, ce tampon mémorise des entiers, un par pixel.
La carte graphique peut gérer automatiquement ce tampon et lire ou écrire dedans sans aucun problème.
VII. ROP▲
Finalement, un ROP ressemble à ceci :
Navigation▲
Tutoriel précédent : unités de texture |
Tutoriel suivant : Early-Z |