Le premier jeu à utiliser de la "vraie" 3D fût le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D.
Les bases du rendu 3D.
Une scène 3D est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme.
Les objets 3D et leur géométrie.
Les objets placés dans la scène 3D sont composés de formes de base, combinées les unes aux autres pour former des objets complexes. En théorie, les formes géométriques en question peuvent être n'importe quoi : des triangles, des carrés, des courbes de Béziers, etc. En général, on utilise des polygones par simplicité, les autres solutions étant peu pratiques et plus complexes. Dans la quasi-totalité des jeux vidéo actuels, les objets sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de maillage, ("mesh" en anglais). Il a été tenté dans le passé d'utiliser des carrés/rectangles (rendu dit en "quad") ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.
Les modèles 3D sont définis par leurs sommets, aussi appelés vertices dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Un triangle est donc composé de 9 coordonnées, 3 par sommet. Ensuite, les sommets sont reliés entre eux. Un segment qui connecte une paire de sommets s'appelle une arête, comme en géométrie élémentaire. Si plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une "face", ou encore une primitive. Je le répète, mais les seules faces actuellement utilisées en rendu 3D sont les triangles. Quand plusieurs faces sont sur un même plan, elles forment un "polygone", bien que le terme ne soit utilisé comme en mathématique. Un assemblage de plusieurs "polygones" donne une surface. Dans la suite du cours, nous parlerons surtout des sommets et des primitives.
La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets, pouvant contenir une certaine redondance ou des informations en plus. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie.
Les textures.
Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des textures, des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief.
Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des texels. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres.
Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique.
Plaquer une texture sur un objet consiste à attribuer un texel à chaque sommet, ce qui est fait lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. Chaque sommet est associé à des coordonnées de texture, qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. Dans les faits, on n'utilise pas de coordonnées entières de ce type. Dans la pratique, les coordonnées de texture sont deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. Le nom donnée à cette technique de description des coordonnées de texture s'appelle l'UV Mapping.
La caméra : le point de vue depuis l'écran.
Outre les objets proprement dit, on trouve une caméra, qui représente les yeux du joueur. Cette caméra est définie par :
Ce qui est potentiellement visible du point de vue de la caméra est localisé dans un volume, situé entre le plan de l'écran et le plan limite, appelé le view frustum. Suivant la perspective utilisée, ce volume n'a pas la même forme. Avec la perspective usuelle, le "view frustum" ressemble à un trapèze en trois dimensions, délimité par plusieurs faces attachées au bords de l'écran et au plan limite. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le "view frustum" est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels.
Le "culling" et le "clipping".
Un point important du rendu 3D est que ce qui n'est pas visible depuis la caméra ne doit idéalement pas être calculé. A quoi bon calculer des choses qui ne seront pas affichées, après tout ? Ce serait gâcher de la puissance de calcul. Et pour cela, les cartes graphiques et les moteurs graphiques incorporent de nombreuses optimisations pour éliminer les calculs inutiles. Diverses techniques de clipping ou de culling existent pour cela. La différence entre "culling" et "clipping" n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme "culling".
La première forme de "culling" est le "view frustum culling", dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du "view frustum". Elle fait que ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Les autres formes de "culling" visent à éliminer ce qui est dans le "view frustum", mais qui n'est pas visible depuis la caméra. Par exemple, il se peut que certains objets situés dans le "view frustum" ne soient pas visibles ou alors seulement partiellement. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. ces deux cas correspondent à deux types de "culling". L'élimination des objets masqués par d'autres est appelé l"'occlusion culling". L'élimination des parties arrières d'un objet est appelé le "back-face culling". Enfin, certains objets qui sont trop loin ne sont tout simplement pas calculés et remplacé par du brouillard ou un autre artefact graphique, voire pas remplacé du tout.
Pour résumer, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. Les cartes graphiques embarquent divers méthodes de "culling" pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le "culling" peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre.
Les effets de brouillard.
Les effets de brouillard sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur), mais ils ont surtout été utilisés pour économiser des calculs. Sur les vieux jeux vidéo, notamment sur d'anciennes consoles de jeux, les effets de brouillards étaient utilisés pour ne pas calculer les graphismes au-delà d'une certaine distance. L'idée est de réduire la taille du "view frustum", en faisant en sorte que le plan limite au-delà duquel on ne voit pas les objet soit assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure dans le rendu qui se verra et sera non seulement inesthétique, mais pourrait aussi passer pour un bug. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance.
La rastérisation et le pipeline graphique.
Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le "lancer de rayons" et la "rasterization". Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. La raison est qu'il demande beaucoup de puissance de calcul, ce qui le rend peu adapté aux jeux vidéo. De plus, le matériel pour accélérer le lancer de rayon n'est pas très efficace et n'a pas pris commercialement. Ce qui explique que depuis un bon moment, les jeux vidéo utilisent la "rasterization". Celle-ci calcule une scène 3D intégralement, avant de faire des transformations pour n'afficher que ce qu'il faut à l'écran.
Avec la rastérisation, le calcul d'une image rendue en 3D passe par une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le pipeline graphique. Précisons cependant que la succession d'étape que nous allons voir n'est pas aussi stricte que vous pouvez le penser. En fait, il n'existe pas un pipeline graphique unique : chaque API 3D fait à sa sauce, le matériel ne respecte pas forcément cette succession d'étapes, et j'en passe. Cependant, toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline que nous allons décrire colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes).
Le cas le plus simple ne demande que trois étapes :
Voyons cela plus en détail. Précisons avant toute chose que nous allons omettre certaines étapes facultatives, afin de simplifier l’exposé. Par exemple, nous n'allons pas parler de l'étape de tesselation, qui permet de rajouter de la géométrie, ce qui sert à déformer les objets ou à augmenter leur réalisme.
Le traitement de la géométrie.
Le traitement de la géométrie se fait en plusieurs étapes.
La première étape est appelée étape de transformation effectue plusieurs changements de coordonnées pour chaque sommet.
Lors de la seconde étape de traitement de la géométrie, les sommets sont éclairés dans une phase d'éclairage. Lors de cette phase, tout sommet se voit attribuer une couleur, qui définit son niveau de luminosité. Celui-ci indique si le sommet est éclairé ou est dans l'ombre.
Éventuellement, diverses opérations de culling sont ensuite effectuées à la fin de cette étape, notamment le clipping et le "view frustum-culling".
Après toutes ces étapes, une fois que les sommets ont tous étés traités, ils sont assemblés en primitives, c'est à dire en triangles ou en carrés/rectangles. L'assemblage est absolument nécessaire pour la prochaine étape du pipeline graphique, qui manipule des primitives. Il s'agit de l'étape d'assemblage de primitives ("primitive assembly").
L'étape de rastérisation.
L'étape de rastérisation effectue beaucoup de choses différentes, même pour faire un rendu basique. Elle fait le lien entre triangle et pixels, diverses opérations d'interpolation, plaque les textures, effectues divers tests de visibilité, mélange les couleurs, écrit dans le "framebuffer". Au niveau du matériel, ainsi que dans la plupart des API 3D, l'étape de rastérisation est séparée en trois/quatre étapes séparées, réalisées par des circuits séparés. Cette séparation en trois étapes est vraiment importante. Elle est commune à tous les API 3D modernes, en plus d'avoir une certaine logique. Après, les trois étapes en question sont elles-mêmes subdivisées en plusieurs étapes distinctes, qui s'enchainent dans un ordre bien précis.
Pour simplifier, le premier circuit s'occupe la traduction des formes (triangles) rendues dans une scène 3D en un affichage pixelisé à l'écran. Cette étape de rastérisation détermine à quoi ressemble la scène visible sur l'écran. Lors de cette étape, chaque pixel de l'écran se voit attribuer un ou plusieurs triangle(s). Pour mieux comprendre quels triangles sont associés à tel pixel, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associé au pixel correspondant.
C'est aussi lors de cette étape que sont appliquées certaines techniques de "culling", qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. Mais surtout, c'est lors de cette étape que chaque pixel se voit attribuer les coordonnées de texture nécessaire pour plaquer la texture dessus. Avant le rendu 3D, les coordonnées de texture sont associée à un sommet. Or, si la rastérisation associe un triangle à chaque pixel, les coordonnées de texture des sommets de ce triangle sont aussi transférées au pixel. Une fois que la carte graphique sait quelles sont les coordonnées de texture associée au pixel, elle lit la texture et utilise les informations lues pour colorier le pixel. C'est la phase de placage de textures. Rastérisation et gestion des textures sont deux opérations très liées entre elles, comme nous le verrons dans quelques chapitres. 
Après l'étape de placage de textures, la carte graphique enregistre le résultat en mémoire. Lors de cette étape, divers traitements de post-processing sont effectués et divers effets peuvent être ajoutés à l'image. Uun effet de brouillard peut être ajouté, des tests de profondeur sont effectués pour éliminer certains pixels cachés, l'antialiasing est ajouté, on gère les effets de transparence, etc. Un chapitre entier sera dédié à ces opérations.
Ce qu'il faut retenir, c'est que la rastérisation se fait en trois : étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales.
Les API 3D.
De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation.
La description du pipeline graphique.
L'API ne fait pas que fournir des morceaux de code que les programmeurs peuvent utiliser. Elles fournissent des contraintes et des règles de programmation assez importantes. Notamment, elles décrivent le pipeline graphique à utiliser. Pour résumer, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Cela signifie que le pipeline graphique varie suivant l'API. Par exemple les API récentes supportent certaines étapes comme l'étape de tesselation, qui sont absentes des API anciennes.
Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline décrit par penGL 1.0 est illustré ci-dessous. On voit qu'il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie ("per vertex operations" et "primitive assembly"), la rastérisation, et les traitements sur les pixels ("per fragment opertaions"). On y voit la présence du "framebuffer" et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La "display list" est une liste de commande de rendu que la carte graphique doit traiter d'un seul bloc, chaque "display list" correspond au rendu d'une image, pour simplifier. Les étapes "evaluator" et "pixel operations" sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques.
Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici un schéma qui décrit le pipeline de DirextX 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc.
L'implémentation peut être logicielle ou matérielle.
La mise en œuvre d'une API 3D peut se faire en logiciel, en matériel ou les deux. En théorie, elles peuvent être implémentées en logiciel, sans avoir recours à une carte graphique : une simple carte d'affichage suffit, le processeur peut faire tout le travail. Et c'était le cas avant l'invention des premières cartes accélératrices 3D. Les toutes premières versions des API OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Par exemple, les accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X, et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D.
Par la suite, avec l'invention des cartes accélératrices 3D, les API se sont adaptées. Les premières cartes graphiques étaient les cartes Voodoo de 3dfx interactive et elles disposaient de leur propre API graphique, la fameuse API Glide. Elle facilitait la gestion de la géométrie et des textures, ce qui collait bien avec l'architecture de ces cartes 3D. Mais elle tomba rapidement en désuétude avec l'évolution de DirectX et d'OpenGL. A l'époque, OpenGL était l'API plébiscitée, car elle était déjà bien implantée dans le domaine industriel, que la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était plus simple à programmer. En comparaison, le DirectX de l'époque avait la réputation d'être absolument nullissime. OpenGL était alors implémenté en logiciel par le système d'exploitation, car les cartes graphiques ne supportaient pas toutes les fonctionnalités de l'API.
Un tournant dans l'évolution d'OpenGL fut la sortie du jeu Quake, d'IdSoftware. Celui-ci pouvait fonctionner en rendu logiciel, mais le programmeur responsable du moteur 3D (le fameux John Carmack) ajouta une version OpenGL du jeu. Le fait que le jeu était programmé sur une station de travail compatible avec OpenGL faisait que ce choix n'était si stupide, même si aucune carte accélératrice de l'époque ne supportait OpenGL. C'était là un choix qui se révela visionnaire. En théorie, le rendu par OpenGL aurait dû se faire intégralement en logiciel, sauf sur quelques rares stations de travail adaptées. Mais 3dfx ajouta un support minimal d'OpenGL, contenant uniquement les fonctionnalités nécessaires pour Quake. John Carmack le dit lui-même, dans le readme.txt de GLQuake :
Par la suite, de nombreux autres fabricants de cartes 3D firent la même chose que 3dfx. C'était l'époque du miniGL, des implémentations partielles d'OpenGL, fournies par les fabricants de cartes 3D, implémentées dans les pilotes de périphériques de ces dernières. Les fonctionnalités d'OpenGL implémentées dans ces pilotes étaient presque toutes exécutées en matériel, par la carte graphique. Un cap fût franchi avec la sortie de la fameuse Geforce 256, qui incorpora la phase de T&L ("Transform & Lighting") d'OpenGL directement en hardware. La phase de traitement géométrique d'OpenGl était réalisée directement par la carte graphique, au lieu de l'être en logiciel.
Avec l'évolution du matériel, les pilotes de périphériques devinrent de plus en plus complets, au point de devenir des implémentations totales d'OpenGL. De nos jours, les API sont surtout implémentées en matériel, la quasi-totalité des fonctionnalités des API étant implémentées soit avec des "shaders", soit avec des circuits non-programmables.
L'API impose des contraintes sur le matériel.
Plus haut, j'ai dit que les API imposent un ordonnancement précis, décrit par les étapes du pipeline. Le matériel doit respecter cet ordonnancement et faire comme si tout le pipeline graphique décrit par l'API était respecté. Je dis faire comme si, car il se peut que le matériel ne respecte pas cet ordre à la lettre. Il le fera dans les grandes lignes, mais il arrive que la carte graphique fasse certaines opérations en avance, comparé à l'ordre imposé par l'API, pour des raisons de performance. Typiquement, effectuer du "culling" ou les tests de profondeur plus tôt permet d'éviter de rejeter de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. Les écarts à l'ordonnancement de l'API sont corrigés d'une manière ou une autre par le hardware, par des circuits spécialisés de remise en ordre.
De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D.
L'architecture d'une carte 3D.
Toute carte graphique contient obligatoirement des circuits essentiels qui ne sont pas liés au rendu 3D proprement dit, mais sont nécessaires pour qu'elle fonctionne : de la mémoire vidéo, des circuits de communication avec le bus, des circuits d’interfaçage avec l'écran, et d'autres circuits. À cela, il faut ajouter les circuits pour le rendu 3D proprement dit, qui comprennent un ensemble hétérogène de circuits aux fonctions fort différentes. Les circuits non-liés au rendu 3D sont globalement les mêmes que pour une carte d'affichage 2D, aussi nous n'en reparlerons pas ici et nous allons nous concentrer sur les circuits du pipeline graphique.
Les architectures en mode immédiat et en "tiles".
Il existe deux types de cartes graphiques : celles en mode immédiat, et celles avec un rendu en "tiles". Les deux ont sensiblement les mêmes circuits, à quelques différences près, mais elles les utilisent d'une manière fort différente. Dans la suite de ce cours, nous parlerons surtout des cartes graphiques en mode immédiat, les architectures en "tile" étant reléguées à la fin du cours. Nous faisons ainsi car les cartes graphiques des ordinateurs de bureau ou portables sont toutes en mode immédiat, alors que les cartes graphiques des appareils mobiles, smartphones et autres équipements embarqués ont un rendu en "tiles". Les raisons à cela sont multiples. La première est que les architectures en "tiles" sont considérées comme moins performantes que celles en mode immédiat, et elles sont d'autant moins performantes que la géométrie de la scène 3D est complexe. Les architectures en "tiles" sont donc utilisées pour les équipements où la performance n'est pas une priorité, comme les appareils mobiles, alors que le rendu en mode immédiat est utilisé pour les ordinateurs performants (de bureau ou portable). Par contre, le rendu en "tiles" est plus simple, plus facile à implémenter en matériel. Une autre caractéristique est un avantage du rendu en tile pour le rendu en 2D, comparé aux architectures en mode immédiat, qui se marie bien aux besoins des smartphones et autres objets connectés.
Mais expliquons donc quelle est la différence entre ces deux types de cartes graphiques. Avec le rendu en "tiles", la géométrie est intégralement rendue avant de faire la rastérisation. De plus, la rastérisation découpe l'écran en l'écran en carrés/rectangles, appelés des "tiles", qui sont rendus séparément, les unes après les autres. Une carte graphique en "tile" calcule toute la géométrie, mémorise le résultat du rendu géométrique en mémoire vidéo, découpe l'écran en "tiles" qui sont rastérisées et texturées une par une. Cette forme de rendu est compatible avec la séparation du pipeline en deux étapes : une étape de traitement géométrique, et une étape de rastérisation proprement dite. Mais les cartes graphiques en mode immédiat ne font pas cela : la rastérisation se fait pixel par pixel, pas par "tiles".
Pour comprendre quelles sont les implications en termes de performance, nous allons séparer la carte graphique en deux circuits : un qui gère la géométrie, et un qui gère la rastérisation au sens large. Nous allons les appeler l'unité géométrique et le rastériseur, respectivement. Les deux lisent ou écrivent des données dans la mémoire vidéo. L'unité géométrique lit en mémoire vidéo la scène 3D, qui est mémorisée dans un tampon de sommets. De même, les textures sont lues dans la mémoire vidéo. Enfin, une fois qu'on a finit de calculer un pixel, il doit être enregistré dans le "framebuffer". Cela est vrai pour les cartes graphiques en rendu immédiat et en rendu à tiles. Les cartes graphiques ajoutent souvent des mémoires caches pour la géométrie et les textures, afin de rendre leur accès plus rapide. Le tout est illustré ci-dessous. Nous n'avons pas parlé de la manière dont communiquent le rastériseur et l'unité géométrique, volontairement, car c'est là que se situe la différence entre les deux types de cartes 3D.
Avec les cartes graphiques en mode immédiat, l'unité géométrique envoie ses résultats à l'unité de rastérisation, sans passer par la mémoire. Et là, un premier problème survient. Un triangle dans une scène 3D correspond généralement à plusieurs pixels. Ce qui fait qu'il y a un déséquilibre entre géométrie et rastérisation : la rastérisation prend plus de temps de calcul que la géométrie. Une conséquence est qu'il arrive fréquemment que le circuit de rastérisation soit occupé, alors que l'unité de géométrie veut lui envoyer des données. Dans ce cas, on doit mettre en attente les résultats géométriques dans une mémoire tampon, appelé le tampon de primitives. De plus, la gestion de la rastérisation demande d'utiliser des informations assez lourdes, qui sont mémorisées en mémoire vidéo, comme le z-buffer dont nous parlerons à la fin du cours.
Avec les cartes graphiques en rendu à tiles, les choses sont totalement différentes. Premièrement, l'unité géométrique n'envoie pas ses résultats au rastériseur, mais mémorise le tout en mémoire vidéo. Plus précisément, les résultats sont enregistrés tile par tile. Le rastériseur lit alors la géométrie d'une tile depuis la mémoire vidéo, et la rastérise. L'avantage est que la tile est tellement petite qu'elle est intégralement rastérisée sans avoir à passer par la mémoire vidéo. En effet, le rastériseur incorpore une petite mémoire SRAM qui mémorise la tile et toutes les informations nécessaires pour effectuer le rendu. Pas besoin d’accéder à un gigantesque z-buffer pour toute l'image, juste besoin d'un z-buffer minuscule pour la tile en cours de traitement, qui tient totalement dans la SRAM pour la tile.
Maintenant, faisons le bilan au niveau des accès mémoire. La performance d'une carte graphique est limitée par la quantité d'accès mémoire qu'on peut effectuer par seconde. Autant dire que les économiser est primordial. Avec une architecture en mode immédiat, le tampon de primitives évite d'avoir à passer par la mémoire vidéo. Par contre, la rastérisation utilise énormément d'accès mémoire pour rendre les pixels, notamment à cause de cette histoire de z-buffer. Peu d'accès mémoire liés à la géométrie, mais la rastérisation est gourmande en accès mémoire. Avec les architectures à tile, c'est l'inverse. Toute la géométrie est enregistré en mémoire vidéo, mais la rastérisation ne fait que très peu d'accès mémoire grâce à la SRAM pour les tiles. Au final, les deux architectures sont optimisées pour deux types de rendus différents. Les cartes à rendu en tuile brillent quand la géométrie n'est pas trop compliquée, mais que la rastérisation et les traitements des pixels sont lourds. Les cartes en mode immédiat sont elles douées pour les scènes géométriquement lourdes, mais avec peu d'accès aux pixels. Le tout est limité par divers caches qui tentent de rendre les accès mémoires moins fréquents, sur les deux types de cartes, mais sans que ce soit une solution miracle.
Un avantage des GPU en rendu à tile est que l’antialiasing est plus rapide. Pour ceux qui ne le savent pas, l'antialiasing est une technique qui améliore la qualité d’image, en simulant une résolution supérieure. Une image rendue avec antialiasing aura la même résolution que l'écran, mais n'aura pas certains artefacts liés à une résolution insuffisante. Et l'antialiasing a lieu intégralement dans les circuits situés dans l'étape de rastérisation ou après. Tout se passe donc dans le rastériseur. Les GPU en mode immédiat disposent de techniques d'optimisations pour l’antialiasing, mais il n’empêche que le GPU doit mémoriser des informations en mémoire vidéo pour faire un antialiasing correct. Alors qu'avec le rendu en tiles, tout l’antialiasing se passe dans le circuit de rastérisation, sans avoir à passer par la mémoire vidéo. La SRAM qui mémorise la tile en cours suffit pour l'antialiasing. L'antialiasing est donc plus rapide.
Les carte graphique modernes sont partiellement programmables.
Les cartes graphiques modernes sont capables d’exécuter des programmes informatiques, comme le ferait un processeur d'ordinateur normal. Les programmes informatiques exécutés par la carte graphique sont appelés des shaders. Les "shaders" sont classifiés suivant les données qu'ils manipulent. On parle de pixel shader pour ceux qui manipulent des pixels, de vertex shaders pour ceux qui manipulent des sommets, de geometry shader pour ceux qui manipulent des triangles, de "hull shaders" et de "domain shaders" pour la tesselation, de "compute shader" pour des opérations sans lien avec le rendu 3D, etc. Ils sont écrits dans un langage de haut-niveau, le HLSL pour les shaders Direct X et le GLSL pour les shaders Open Gl, puis sont ensuite traduit (compilés) en instructions machines compréhensibles par la carte graphique. Au début, ces langages et la carte graphique supportaient uniquement des opérations simples. Mais au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches à chaque version de Direct X ou d'Open Gl, et le matériel en a fait autant. Les premiers shaders apparus étaient les "vertex shaders", avec la Geforce 3 qui a rendu la gestion de la géométrie programmable. Par la suite, l'étape de traitement des pixels est elle aussi devenue programmable et les pixels shaders ont fait leur apparition. Puis d'autres formes de shaders sont apparues, pour effectuer des calculs géométriques complexes ou des calculs non-graphiques.
Les "shaders" sont exécutés par un ou plusieurs processeurs intégrés à la carte graphique, qui portent le nom de processeurs de "shaders". Les cartes graphiques récentes contiennent un grand nombre de processeurs de shaders, plusieurs centaines. Mais une carte graphique moderne ne contient pas que des processeurs de shaders, mais aussi des circuits spécialisés dans le rendu 3D qui prennent en charge une étape du pipeline graphique, appelés des unités de traitement graphiques. On trouve ainsi une unité pour le placage de textures, une unité pour l'étape de transformation, une pour la gestion de la géométrie, une unité de rasterization, une unité d'enregistrement des pixels en mémoire appelée ROP, etc. Les unités sont divisés en deux catégories : d'un côté les circuits non-programmables (dits fixes) et de l'autre les circuits programmables (les processeurs de "shaders"). Globalement, la gestion de la géométrie et des pixels est programmable, mais la rasterization, le placage de texture, le "culling" et l'enregistrement du "framebuffer" ne l'est pas.
Outre les unités du paragraphe précédent, une carte graphique contient d'autres circuits qui s'occupent de la logistique, du transfert de données ou de la répartition du travail entre les unités de traitement proprement dites. Le tampon de primitive vu plus haut en est un exemple et d'autres mémoires tampon de ce type existent. L'un de ces circuits de logistique est le processeur de commandes, dont nous parlerons dans quelques chapitres. Pour simplifier, il commande les différentes étapes du pipeline graphique et s'assure que les étapes s’exécutent dans le bon ordre.
Ce qu'il faut comprendre, c'est qu'une carte graphique moderne est un mélange de circuits non-programmables et de circuits programmables. L'assemblage de circuits fixes et de processeurs de "shaders" peut sembler contradictoire. Pourquoi ne pas tout rendre programmable ? Ou au contraire, pourquoi ne pas utiliser seulement des circuits fixes ? Le choix entre les deux parait simple. Les circuits programmables sont légèrement plus lents, mais plus flexibles, plus simples à concevoir et facilitent la vie aux programmeurs. Par contre, les circuits fixes sont plus rapides, plus économes en énergie, utilisent moins de transistors, mais sont plus compliqués à concevoir et moins flexibles. Mélanger les deux semble étrange.
La réponse rapide est qu'il s'agit d'un compromis entre flexibilité et performance qui permet d'avoir le meilleur des deux mondes. Rendre la gestion de la géométrie ou des pixels programmable est très intéressant pour le programmeur, ce qui explique qu'on a des processeurs de "shader" pour les "vertex shaders" et "pixel shaders". par contre, le programmeur ne gagne pas grand chose à avoir un rastériseur programmable, mais les performances de rastérisation sont cruciales. Aussi, ce circuits est laissé non-programmable et conçu avec un circuit fixe plus rapide. Dans la fin de ce chapitre, et dans le chapitre suivant, nous allons étudier comment cette recherche du compromis a donné naissance aux cartes graphiques modernes. Nous allons voir l'évolution dans le temps des cartes graphiques à la fin de ce chapitre. Dans le chapitre suivant, nous allons voir pourquoi les "vertex shaders" et les "pixels shaders" sont apparus, en prenant l'exemple des algorithmes d'éclairage.
Un historique simplifié du hardware des cartes graphiques.
Le hardware des cartes graphiques a fortement évolué dans le temps, ce qui n'est pas une surprise. Les évolutions de la technologie, avec la miniaturisation des transistors et l'augmentation de leurs performances a permit aux cartes graphiques d'incorporer de plus en plus de circuits avec les années. Avant l'invention des cartes graphiques, toutes les étapes du pipeline graphique étaient réalisées par le processeur : il calculait l'image à afficher, et l’envoyait à une carte d'affichage 2D. Au fil du temps, de nombreux circuits furent ajoutés, afin de déporter un maximum de calculs vers la carte vidéo. Les GPU actuels s'occupent de tous les traitements liés au moteur graphique.
Dès le début de la 3D, le choix entre circuits fixe et circuits programmables s'est fait ressentir. Les toutes premières cartes graphiques se classaient en deux camps : d'un côté les cartes graphiques complètement non-programmables, de l'autre des GPU totalement programmables. La toute première carte 3D était du second camp : c’était la Rendition Vérité V1000. Elle contenait un processeur MIPS cadencé à 25 MHz, 4 mébioctets de RAM, une ROM pour le BIOS, et un RAMDAC, rien de plus. C'était un vrai ordinateur complètement programmable de bout en bout. Les programmeurs ne pouvaient cependant pas utiliser cette programmabilité avec des "shaders", mais elle permettait à Rnedition d'implémenter n'importe quelle API 3D, que ce soit son API propriétaire ou OpenGL, voire DirectX. Mais les performances s'en ressentaient. Réaliser la rastérisation et le placage de texture en logiciel n'est pas efficace, pareil pour les opérations de fin de pipeline comme l'antialiasing. Au final, sa programmabilité était un plus pour la compatibilité logicielle, mais qui avait un cout en termes de performances. Le manque d'unités fixes très rapides pour la rastérisation, le placage de texture ou les opérations de fin de pipeline était clairement un gros défaut.
Le second camp, celui des cartes graphiques totalement non-programmables et ne contenant que des circuits fixe, regroupe les Voodoo de 3dfx, les Riva TNT de NVIDIA, les Rage/3D d'ATI, la Virge/3D de S3, et la Matrox Mystique. Elles contenaient des circuits pour gérer les textures, quelques effets graphiques (brouillard) et l'étape d'enregistrement des pixels en mémoire. Par la suite, ces cartes s’améliorèrent en ajoutant plusieurs circuits de gestion des textures, pour colorier plusieurs pixels à la fois. Cela permettait aussi d'utiliser plusieurs textures pour colorier un seul pixel : c'est ce qu'on appelle du "multitexturing". Les performances avec ces cartes étaient nettement meilleures que pour la Rendition V1000. Le fait d'avoir des circuits câblés rapides pour certaines tâches bien choisie donnait de bonnes performances, et personne n'avait la moindre utilité pour des circuits programmables à l'époque.
Les cartes suivantes ajoutèrent une gestion des étapes de "rasterization" directement en matériel. Les cartes ATI rage 2, les Invention de chez Rendition, et d'autres cartes graphiques supportaient ces étapes en hardware. De nos jours, ce genre d'architecture est commun chez certaines cartes graphiques intégrées dans les processeurs ou les cartes mères.
La première carte graphique capable de gérer la géométrie fût la Geforce 256, la toute première Geforce. Son unité de gestion de la géométrie n'est autre que la bien connue T&L (Transform And Lighting). Elle implémentait des algorithmes d'éclairage de la scène 3D assez simples, comme un éclairage de Phong, qui étaient directement câblés dans ses circuits. Mais le défaut de cette approche était que ces cartes graphiques incorporaient des algorithmes d'éclairage très spécifiques, alors qu'il existe de très nombreux algorithmes d'éclairage, au point où on ne peut pas tous les mettre dans une carte graphique. Les programmeurs avaient donc le choix entre programmer les algorithmes d’éclairage qu'ils voulaient ou utiliser ceux de la carte graphique.
À partir de la Geforce 3 de Nvidia, les unités de traitement de la géométrie sont devenues programmables. L'avantage était que les programmeurs n'étaient pas limités aux algorithmes implémentés dans la carte 3D pour obtenir de bonnes performances. Ils pouvaient programmer l'algorithme d'éclairage qu'ils voulaient et pouvaient le faire exécuter directement sur la carte graphique. Les unités de traitement de la géométrie deviennent donc des processeurs indépendants, capables d’exécuter des programmes appelés Vertex Shaders. Ces "shaders" sont écrits dans un langage de haut-niveau, le HLSL ou le GLSL, et sont traduits (compilés) par les pilotes de la carte graphique avant leur exécution. Au fil du temps, les spécifications de ces langages sont devenues de plus en plus riches et le matériel en a fait autant. D'autres types de "shaders" ont été inventés : "pixels shaders" pour le traitement de pixels, "shaders" de géométrie, "shaders" génériques, etc. Les premiers à faire leur apparition furent les "pixel shaders", suivis par les "geometry shaders" et les "shaders" pour la tesselation.
Pour résumer, l'évolution des cartes 3D s'est faite d'une manière assez complexe. Le compromis entre circuits fixes et programmables a été difficile à trouver. Les concepteurs ont procédé par essais et erreurs, jusqu'à trouver le compromis actuel. Les premières cartes graphiques sont parties sur deux pistes totalement opposées : d'un côté une carte totalement programmable, de l'autre des cartes avec des circuits fixes non-programmables. L'échec de la Rendition Vérité 1000, totalement programmable mais avec de trop faibles performances, a orienté le marché dans la seconde direction. Les cartes 3D ont alors incorporé des circuits de rastérisation et de placage/filtrage de texture, qui sont toujours là de nos jours. Elles ont ensuite intégré des circuits de calculs géométrique, avant de revenir en arrière pour les remplacer par des circuits programmables. Leur programmabilité s'est ensuite accrue, en incorporant des processeurs capables d’exécuter n'importe quel algorithme, mais sans revenir sur les circuits fixes établis. Expliquer pourquoi demande cependant d'y passer un chapitre entier, ce qui est le sujet du chapitre suivant.