Aller au contenu

Les cartes graphiques/Le rendu d'une scène 3D : concepts de base

Un livre de Wikilivres.

Le premier jeu à utiliser de la "vraie 3D" texturée fut 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

[modifier | modifier le wikicode]

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

[modifier | modifier le wikicode]
Illustration d'un dauphin, représenté avec des triangles.

Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D 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 quadrilatères (rendu dit en quad) ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues.

Exemple de modèle 3D.

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. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des quads, et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des primitives.

La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. 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.

La caméra : le point de vue depuis l'écran

[modifier | modifier le wikicode]

Outre les objets proprement dit, on trouve une caméra, qui représente les yeux du joueur. Cette caméra est définie au minimum par :

  • une position ;
  • par la direction du regard (un vecteur).

À la caméra, il faut ajouter tout ce qui permet de déterminer le champ de vision. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une pyramide de vision dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. À l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le viewport.

Caméra.
Volume délimité par la caméra (view frustum).

La majorité des jeux vidéos ajoutent deux plans :

  • un near plane en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches.
  • Un far plane, un plan limite au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains.

Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le view frustum. 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.

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.

Texture Mapping

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 peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas.

La différence entre rastérisation et lancer de rayons

[modifier | modifier le wikicode]

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. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal.

La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la rasterization.

La rastérisation est structurée autour de trois étapes principales :

  • Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D.
  • Une étape de traitement de la géométrie, qui gère tout ce qui a trait aux sommets et triangles.
  • Une étape de rastérisation qui détermine sur quels pixels de l'écran est affiché le triangle.
  • Une étape de traitement des pixels, qui colorie les pixels et gère les textures.

Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée.

Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du pipeline. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents.

Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec 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, qui sera détaillé dans ce qui suit.

Le calcul de la géométrie

[modifier | modifier le wikicode]

Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l'étape de transformation : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective.

Les trois étapes de transformation

[modifier | modifier le wikicode]

La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour.

De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets..

Transformations géométriques possibles pour chaque triangle.

Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z).

Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.

Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du view frustum sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le view frustum. Dans le cas qui nous intéresse, le view frustum passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces.

Les changements de coordonnées se font via des multiplications de matrices

[modifier | modifier le wikicode]

Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc.

Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail.

Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble.

Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc.

Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps.

L'élimination des surfaces cachées

[modifier | modifier le wikicode]

Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de clipping ou de culling. 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.

Les cartes graphiques modernes embarquent diverses 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 différentes formes de culling/clipping

[modifier | modifier le wikicode]

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. 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. Le view frustum culling est assez trivial : il suffit d'éliminer ce qui n'est pas dans le view frustum avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le view frustrum, ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable.

View frustum culling : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.

Les autres formes de culling visent à éliminer ce qui est dans le view frustum, mais qui n'est pas visible depuis la caméra. 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. Dans les deux cas, nous parlerons d'élimination des surfaces cachées.

Occlusion culling : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.

Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger.

L'occlusion culling demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la profondeur du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z.

L'algorithme du peintre

[modifier | modifier le wikicode]

Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l'algorithme du peintre.

Polygons cross

Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal.

Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU.

Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de portal rendering, soit un système de Binary Space Partionning, assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment.

Le tampon de profondeur

[modifier | modifier le wikicode]
Z-buffer correspondant à un rendu

Une autre solution utilise ce qu'on appelle un tampon de profondeur, aussi appelé un z-buffer. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du far plane du viewfrustum. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.

Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur.

Illustration du processus de mise à jour du Z-buffer.

Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives.

Toutes les cartes graphiques modernes utilisent un système de z-buffer. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective.

Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de z-fighting. Voici ce que cela donne :

Z-fighting

Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde.

La rastérisation et les textures

[modifier | modifier le wikicode]

Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple 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.

La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (UV Mapping). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures.

Le rendu en fil de fer

[modifier | modifier le wikicode]
Rendu en fil de fer d'un objet 3D.

Le rendu 3D en fils de fer est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS.

Maze war
Maze representation using wireframes 2022-01-10

Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. À la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. À la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet.

L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le framebuffer les lignes à tracer.

Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le Line Drawing System-1 de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres.

Le rendu à primitives colorées

[modifier | modifier le wikicode]
Exemple de rendu pouvant être obtenu avec des sommets colorés.

Une amélioration du rendu précédent utilise des triangles/quads coloriés. Chaque triangle ou quad est associé à une couleur, et cette couleur est dessinée sur le triangle/quadaprès la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/quad est associé à une couleur, qui est dessinée sur le triangle/quad après la rastérisation. La technique est nommée colored vertices en anglais, nous parlerons de rendu à maillage coloré.

Maillage coloré.

La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/quad rendu correspond à un triangle/quad à l'écran. Et l'intérieur de ce triangle/quad est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les blitters. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/quad. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le blitter les utilise pour colorier la figure géométrique.

Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés.

Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne I Robot d'Atari, voici une vidéo youtube à ce sujet :

Le placage de textures direct

[modifier | modifier le wikicode]

Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le rendu par placage de texture direct en premier. Et nous l'appellerons rendu direct dans ce qui suit, pour simplifier les explications. Le rendu direct est une sorte d'intermédiaire entre rendu 2D à base de sprite et rendu 3D moderne. La géométrie est rendue en 3D pour générer des quads, mais ces quads ne servent à guider la copie des sprites/textures dans le framebuffer.

Le rendu direct peut utiliser aussi bien des triangles que des quads, mais nous allons partir du principe qu'il utilise des quads, à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un quad est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le quad est vu de face, un trapèze si on le voit de biais. Pour résumer, un 'quad' correspond à un quadrilatère sur l'écran. Et le placage de texture est effectué quand on sait quelles sont les coordonnées de ce quadrilatère à l'écran.

Le rendu direct associe à chaque quad, une texture rectangulaire, assez semblable à un sprite si on regarde de très loin. L'idée est de remplir le quadrilatère à l'écran avec ce sprite, cette texture. La seule subtilité est que la texture/sprite doit être déformé pour rentrer dans le quadrilatère, cette déformation dépendant de la perspective, de l'angle entre le quad et la caméra, et ainsi de suite. Une fois déformée, le sprite/texture est directement recopié au bon endroit dans le framebuffer, pour remplir le quadrilatère.

Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du quad, déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un quad, et de faire quelques calculs. N'importe quel VDC incluant un blitter avec une gestion du zoom/rotation des sprites peut le faire. Au-delà de cette déformation, il vaut mieux appliquer un filtre d'interpolation pour lisser l'image des sprite déformés, afin d'obtenir une bonne qualité d'image. Le filtre en question et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.

Exemple caricatural de placage de texture sur un quad.

Un point important est que les quads doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le framebuffer. Quelque chose qui devrait vous rappeler le rendu 2D avec l'algorithme du peintre, où les sprites sont rendus du plus lointain au plus proche.

Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des quads, mais il ne s'agit pas d'une différence stricte. L'usage de triangles/quads peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en quad se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse.

L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du framebuffer est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes.

Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le framebuffer.

Géométrie Processeurs dédiés programmé pour émuler le pipeline graphique
Tri des quads du plus lointain au plus proche Processeur principal (implémentation logicielle)
Application des textures Blitter amélioré, capable de faire tourner et de zoomer sur des sprites.

L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque.

Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D.

Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné.

Le placage de textures inverse

[modifier | modifier le wikicode]

Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le framebuffer. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le placage de texture inverse, aussi appelé l'UV Mapping. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par quad/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, 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. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet.

Exemple de placage de texture.

Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais 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. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le mip-mapping. 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.

UV Mapping

Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le framebuffer. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés.

Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.

Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, 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és au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé.

Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités.

L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres.

La transparence, les fragments et les ROPs

[modifier | modifier le wikicode]

Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées.

La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes.

Cartes accélératrices PC, avant l'arrivée des shaders
Géométrie Rastérisation Placage de textures

Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés.

Cartes accélératrices PC, avant l'arrivée des shaders
Géométrie Rastérisation Tampon de profondeur Placage de textures

En réalité, la profondeur des fragments est gérée par un circuit appelé le Raster Operations Pipeline (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline.

Cartes accélératrices PC, avant l'arrivée des shaders
Géométrie Rastérisation Placage de textures Raster Operations Pipeline

Le mélange alpha

[modifier | modifier le wikicode]

La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque.

La transparence d'un objet/pixel est définie par un nombre, appelé la composante alpha. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d'alpha blending.

Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.

Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de fragment. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles.

Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange alpha pour cela. Il suffit de faire le mélange alpha entre le fragment qui vient d'être calculé, et le pixel dans le framebuffer. Pour cela, le fragment a une composante alpha, qui est ajouté aux trois couleurs RGB. Le pixel déjà dans le framebuffer est un résultat temporaire, né du mélange alpha de tous les fragments précédents.

Un défaut de cette méthode est qu'elle fonctionne assez mal avec un tampon de profondeur. Si le tampon de profondeur est activé, le mélange alpha ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Et procéder dans cet ordre a un défaut : on dessine des objets dans le framebuffer, pour qu'ensuite les objets devant écrasent ce qui a déjà été dessiné. Un même pixel peut donc être dessiné plusieurs fois, dont une seule sera pertinente. Et ces écritures utilisent de la bande passante mémoire, qui est une ressource précieuse sur un GPU moderne. Il s'agit d'un phénomène appelé overdraw, ou sur-dessinage en français. Quelques optimisations permettent d'éliminer l'overdraw en rendant les objets du plus proche au plus lointain, d'autres permettent de dessiner des objets dans un ordre arbitraire, mais nous ne pouvons pas en parler ici.

Beaucoup de moteurs 3D rendent séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, ce qui fait qu'on n'a pas à les trier, alors que les objets transparents doivent être triés selon leur distance. un autre avantage est que le mélange alpha est désactivé lors de la première passe, alors que c'est la mise à jour du tampon de profondeur qui est désactivé lors de la seconde passe, ce qui augmente un peu les performances dans les deux cas.

Le test alpha est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc.

Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange alpha. En effet, les opérations de mélange alpha sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le framebuffer, on applique le mélange alpha et on écrit le résultat en mémoire vidéo. L'alpha test permet donc de gagner en performance au prix d'une baisse de la qualité d'image.

Il y a cependant des cas où l'usage du test alpha est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange alpha ne donne pas de bons résultats. L'usage du test alpha permet d'obtenir un rendu correct. Pour d'informations via ce lien :

Les effets de brouillard

[modifier | modifier le wikicode]

Les effets de brouillard sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. Le view frustum utilise alors un plan limite, au-delà duquel on ne voit pas les objets. Mais ce plan limite donne une cassure inesthétique dans le rendu. 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.

Pour calculer le brouillard, on effectue un mélange alpha entre la couleur du pixel et une couleur de brouillard. La différence est que l'on n'utilise pas la transparence pour faire le mélange, mais un coefficient de brouillard, noté .

Le coefficient de brouillard dépend de la coordonnée de profondeur, de la distance du pixel par rapport à la caméra. Le brouillard démarre à une distance , et masque totalement les objets à partir d'une distance . Entre les deux, le coefficient de brouillard dépend de la distance. OpenGL autorise trois formules de calcul suivantes :

L'éclairage d'une scène 3D

[modifier | modifier le wikicode]

L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation.

Les sources de lumière et les couleurs associées

[modifier | modifier le wikicode]

L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous.

Types de sources de lumière.
Source de lumière directionnelle.

Les sources directionnelles servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus.

Les sources ponctuelles sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles.

  • Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des point light dans le schéma du dessus.
  • Les secondes émettent de la lumière dans une direction privilégiée. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des sport light dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
Lumière ambiante.

En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain éclairage ambiant, qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la lumière ambiante. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante.

La lumière incidente : le terme géométrique

[modifier | modifier le wikicode]

Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la lumière incidente.

La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la réflectivité bidirectionnelle. Le terme anglais est bidirectional reflectance distribution function, abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit.

La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond.

Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante.

La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée.

Exemple avec la lumière solaire.
Angle d'incidence.

En clair, tout dépend de l'angle d'incidence de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré.

Normale de la surface.

Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la normale, qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet.

La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc :

Le terme ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le terme géométrique, en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un material, qui indique comment il réfléchit la lumière, ainsi que sa texture.

Le produit scalaire de deux vecteurs

[modifier | modifier le wikicode]

Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le produit scalaire. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté . Le produit scalaire est équivalent à la formule suivante :

, avec A et B la longueur des deux vecteurs A et B.

L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant :

En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué.

Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire.

La réflexion de la lumière sur la surface

[modifier | modifier le wikicode]
Reflection de la lumière sur une surface parfaitement lisse.

Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de réflexion parfaite.

Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de réflexion diffuse.

Différence entre réflexion diffuse et spéculaire.
Réflexion diffuse.

Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le material le plus simple qui soit, appelé un diffuse material.

Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une couleur diffuse à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants :

  • L est le vecteur pour la lumière incidente ;
  • N est la normale du sommet ;
  • I est l'intensité de la source de lumière ;
  • est la couleur diffuse.

Rajoutons maintenant l'effet de la lumière ambiante à un material de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une couleur ambiante, différente de la couleur diffuse.

avec la couleur ambiante du point de surface et l'intensité de la lumière ambiante.

En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une réflexion spéculaire, qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le specular highlight. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses.

Couleurs utilisées dans l'algorithme de Phong.
Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).

Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous :

La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une couleur spéculaire, qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire.

La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution.

Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel

[modifier | modifier le wikicode]

Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet, ce qui fait qu'on parle d'éclairage par sommet, ou vertex lighting. Il attribue une illumination/couleur à chaque sommet de la scène 3D. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles.

Pour obtenir la couleur finale d'un pixel, l'éclairage par sommet fait une moyenne de la couleur de chaque sommet. La moyenne est une moyenne pondérée, qui tient compte de la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, qui est prise en charge par l'étape de rastérisation.

L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l'éclairage par pixel (per-pixel lighting), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le bump/normal mapping.

L'éclairage de Phong calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. À la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel.

Eclairage par sommet.
Eclairage de Phong.
Eclairage par sommet.
Eclairage de Phong.

La technique du normal mapping est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le normal-mapping précalcule les normales d'une surface dans une texture, appelée la normal-map. Lors du rendu 3D, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. Le normal mapping permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Il permet de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser.

Différence sans et avec normal-mapping.

Les shaders : des programmes exécutés sur le GPU

[modifier | modifier le wikicode]

Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures.

Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré circuit de Transform & Lightning, qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des material bien précis (des Phong materials), rien de plus.

Cartes accélératrices PC, avant l'arrivée des shaders
Unité de T&L : géométrie Rastérisation Placage de textures Raster Operations Pipeline

L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de materials possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.

Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les shaders. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : shader pour shading (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage.

L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. À la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables.

Implémentation de l'éclairage sur les cartes graphiques

Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux.

Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les vertex shaders font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture.

Cartes accélératrices PC, avant l'arrivée des shaders
Vertex shader Rastérisation Placage de textures Raster Operations Pipeline
Pixel shader