Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.46.0-wmf.23
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Fonctionnement d'un ordinateur/Les architectures à parallélisme de données
0
65960
763338
762322
2026-04-09T15:40:18Z
Mewtow
31375
/* Les anciennes cartes graphiques hybrides SIMD-VLIW */
763338
wikitext
text/x-wiki
Nous allons maintenant aborder le parallélisme de données, qui consiste à traiter des données différentes en parallèle. De nombreuses situations s'y prêtent relativement bien : traitement d'image, manipulation de sons, vidéo, rendu 3d, etc. Mais pour exploiter ce parallélisme, il a fallu concevoir des processeurs adaptés. Les architectures les plus simples exécutent une instruction sur plusieurs données en parallèle, à l'intérieur d'un processeur. Ces architectures exploitent le parallélisme de données au niveau de l'unité de calcul, celle-ci pouvant exécuter un même calcul sur des données différentes en parallèle.
Si on omet quelques exceptions, on peut classer ces architectures dans plusieurs catégories principales : les processeurs à instructions SIMD, les processeurs à instructions SIMD à prédicat, les processeurs SIMT, les processeurs vectoriels. Si les processeurs vectoriels sont assez rares de nos jours, tous les processeurs récents incorporent des instructions SIMD. Quant au SIMT, il est utilisé sur les cartes graphiques modernes.
==Les instructions SIMD : les points communs entre tous les processeurs SIMD==
Les processeurs modernes fournissent presque tous des '''instructions SIMD''', qui sont capables de traiter plusieurs éléments en parallèle. Elles travaillent sur des nombres entiers ou flottants regroupés dans ce qu'on appelle un ''vecteur'', qui sont eux-mêmes stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128,,256, voire 512 bits, comparé aux 32/64 bits des registres scalaires. Le résultat est que les processeurs SIMD modernes ont des registres SIMD séparés des registres entiers/flottants normaux. Un défaut de cette organisation est que les registres vectoriels sont des registres en plus, qui doivent être sauvegardés lors des commutations de contexte, lors des interruptions, lors des appels systèmes, etc.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Les instructions SIMD peuvent être rassemblées en deux grands groupes : les horizontales et les verticales.
Les '''instructions SIMD verticales''' travaillent en parallèle sur les éléments qui sont "à la même place" dans deux vecteurs. Elles peuvent additionner ou multiplier deux vecteurs, par exemple. Pour prendre l'exemple d'une instruction d'addition vectorielle, celle-ci va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
Les '''instructions SIMD horizontales''' partent d'un vecteur et ont pour résultat un simple nombre. Elles peuvent calculer la somme ou le produit des éléments d'un vecteur, renvoyer le nombre d’éléments nuls dans un vecteur, etc.
[[File:Instructions SIMD.png|centre|vignette|upright=2|Instructions SIMD]]
Les instructions SIMD sont difficilement utilisables dans des langages de haut niveau et c'est donc au compilateur de traduire un programme avec des instructions vectorielles. Les transformations qui permettent de traduire des morceaux de programmes en instructions vectorielles (déroulage de boucles, strip-mining) portent le nom de vectorisation.
===Les instructions SIMD arithmétiques verticales===
Les instructions SIMD sont essentiellement des instructions arithmétiques, comme des additions, des soustractions, des multiplications, éventuellement des opérations bit à bit, et quelques autres. Suivant la taille des données, et le type de celle-si, on devra effecteur des instructions différentes. Par exemple, on devra utiliser deux instructions d'addition différentes suivant qu'on manipule des flottants 64 bits ou des entiers 32 bits. De même, l'instruction pour additionner des vecteurs d'entiers de 16 bits sera différente de celle manipulant des vecteurs d'entiers de 32 bits.
L'addition et la multiplication peuvent générer des débordements d'entier. Pour les instructions non-SIMD, le débordement d'une addition est géré par un bit de retenue, stocké dans le registre d'état. Mais pour les instructions SIMD, ce n'est pas une solution très facile à implémenter. À la place, les additions et soustractions utilisent généralement l'arithmétique saturée, à savoir que lorsqu'une addition déborde, le résultat est la valeur maximale représentable dans un entier.
Pour la multiplication, les instructions non-SIMD génère un résultat qui est codé sur le double du nombre de bits. Une multiplication de deux nombres 64 bits donnera un résultat sur 128 bits, par exemple. Pour gérer cela, les multiplications non-SIMD stockent ce résultat dans deux registres, ou ne conservent que les 64 bits de poids faible. D'autres solutions sont possibles, mais ces deux là sont les plus utilisées. Pour les instructions SMID, une autre solution est préférée car plus simple pour de telles instructions : fournir deux instructions : une qui calcule les 64 bits de poids faible du résultat, une autre pour les 64 bits de poids fort.
Les processeurs SIMD étant utilisés pour du traitement d'image ou du rendu 2D/3D, ils supportent généralement des opérations mathématiques assez complexes sur des nombres flottants. Il n'est pas rare d'avoir des instructions SIMD pour le calcul de l'inverse d'un nombre, de sa racine carrée, l'inverse d'une racine carrée, des fonctions trigonométriques, etc. L'implémentation matérielle de telles instructions est généralement très complexe et gourmande en circuits, ce qui fait que les concepteurs de processeurs rusent. Sauf sur certains processeurs assez peu fréquents, les instructions mathématiques complexes ne calculent pas un résultat exact, mais une approximation du résultat. Utiliser un résultat approximatif ne pose pas de problème pour de telles applications, le rendu d'image ou 3D s'en accommodant parfaitement, contrairement au calcul scientifique.
===Les instructions SIMD de manipulation de données intra-vecteur===
Après avoir vu les instructions SIMD verticales, il est temps de voir les instructions SIMD horizontales. Pour rappel, celles-ci travaillent un vecteur unique, dont elles réarrangent ou modifient les éléments. Tout cela sera plus parlant une fois qu'on aura donné quelques exemples. Mais avant toute chose, nous allons séparer les instructions horizontales en deux sous-types, fondamentalement différents. Le premier sous-type change de place les données dans un vecteur. Le second type est celui des instructions horizontales arithmétiques/logiques habituelles, qu'on verra plus tard. La distinction entre les deux est très importante, comme on le verra plus tard.
[[File:SIMD instruction data movement - One operand.png|vignette|upright=1.5|Instructions SIMD de mouvement de données à une opérande.]]
Les instructions SIMD horizontales les plus communes bougent des données à l'intérieur des vecteurs, ce qui leur vaut le nom d''''instructions de manipulation de vecteur'''. Les plus simples sont celles qui ont une seule opérande, à savoir qui prennent un vecteur comme opérande et renvoient un vecteur résultat.
Les plus intuitives sont les instructions de '''permutation''', dont le nom est assez explicite. Elles changent de place des données dans un vecteur. Dans le cas le plus compliqué, elles prennent deux vecteur : le vecteur dans lequel faire la permutation, et un vecteur qui indique comment faire la permutation. Le vecteur précise, pour chaque élement du vecteur, où il doit aller après la permutation. L'instruction effectue la permutation des entiers/flottants/octets dans le vecteur.
Les instructions '''''compress''''' et '''''expand''''' sont elles aussi spécifiques aux vecteurs. L'instruction ''compress'' prend un vecteur en opérande, sélectionne certaines valeurs présentes dedans, et les stocke dans un vecteur de sortie. Elle regroupe les valeurs sélectionnées dans le début du vecteur, et laisse les cases inoccupées à 0. Par exemple, pour un vecteur de 16 entiers, elle permet de sélectionner 5 entiers dedans, et les place dans un vecteur résultat qui ne contient que ces 5 valeurs au début du vecteur, le reste est à 0. L'instruction ''expand'' fait l'inverse : elle prend un vecteur crée par l'instruction ''compress'' (ou du moin qui a le même format), prend toutes les valeurs non-nulles dedans, et les disperse dans un vecteur de sortie, en les mettant à la place désirée.
Les autres instructions SIMD horizontales prennent deux opérandes, voire aucune opérande vectorielle (elles prennent un nombre, pas un vecteur).
Les instructions '''''shuffle''''' prennent plusieurs vecteurs, sélectionnent les éléments adéquats dans chaque vecteur, et les regroupe dans un vecteur résultat. Par exemple, prenons 2 vecteurs de 16 entiers chacun. Une instruction ''shuffle'' peut prendre 8 entiers dans chaque vecteur et les regrouper dans un vecteur résultat unique de 16 entiers. Ou encore, elle peut prendre trois vecteurs de 16 flottants chacun, prendre 4 flottants dans le premier, 10 dans le second et 2 dans le troisième, et regrouper le tout dans un vecteur résultat de 16 flottants.
Il arrive que les deux cas précédents correspondent à des cas séparés, à deux instructions de type ''shuffle'', mais qui fonctionnent différemment. Le premier type prend les N premiers éléments du premier vecteur, puis prend les élements manquant à partir de la fin du vecteur. Le second type prend un élément sur deux dans chaque vecteur, mais avec un décalage d'un rang entre les deux vecteurs.
Il y a aussi les instructions dites de '''''broadcast''''', qui copient une valeur unique dans un vecteur entier. Elles sont surtout utilisées pour initialiser des tableaux avec une valeur unique, ou pour remplir un vecteur avec des données prédéterminées pour certains calculs impliquant des constantes.
[[File:SIMD instruction exemple.png|centre|vignette|upright=1.5|Instructions SIMD de mouvement de données qui ne sont pas à une opérande.]]
===Les instructions SIMD de réduction===
Il existe des instructions horizontales de type arithmétiques. La plus simple est celle qui additionne les différents entiers/flottants d'un vecteur. Elle est utilisée pour accélérer une opération précise : faire la somme d'un tableau d'entier/flottants. On peut aussi citer les opérations maximum et minimum, qui renvoient le plus grand/petit élément d'un vecteur. Un point important avec ces opérations est qu'elles prennent un vecteur, mais renvoie un résultat unique, un scalaire, un nombre entier/flottant seul. Elles sont parfois appelées des '''instructions de réduction'''.
Les instructions de réduction sont généralement la spécificité des processeurs vectoriels, les processeurs SIMD récents n'en ont pas. La raison est que leur implémentation matérielle est généralement assez compliquée. Contrairement aux instructions verticales, elles ont une forme de parallélisme de donnée très limitée. Elles ne travaillent pas exactement sur des données indépendantes, elles font des calculs dont le résultat est utilisé par d'autres, il y a des dépendances de données entre éléments du vecteur.
Cependant, il faut savoir que l'on peut émuler une instruction de réduction en utilisant des instructions verticales couplées à des instructions de manipulation de vecteur. Par exemples, prenons le cas d'une instruction de réduction qui additionne entre eux les éléments d'un vecteur. On peut l'émuler en utilisant une addition verticale entre deux vecteurs, et une instruction ''compress''. L'idée est la suivante : on coupe le vecteur initial en deux avec deux instructions ''compress'', ce qui donne deux sous-vecteurs. Puis, on additionne les deux vecteurs résultat entre eux. Puis on répète les deux étapes précédentes jusqu'à obtenir la somme finale.
Le fait que l'on puisse émuler les instructions de réduction est la raison principale qui explique que les processeurs SIMD récents n’intègrent pas d'instructions de réduction. Ils se débrouillent avec des instructions SIMD de manipulation de vecteur, et des instructions SIMD verticales. C'est un bon compromis entre cout en circuits et performance.
===Les accès mémoire===
La gestion des accès mémoire est assez hétéroclite, la façon de faire étant différente selon les architectures. Dans le cas le plus simple, les données d'un vecteur sont contigües en mémoire et les instructions ont juste à préciser l'adresse mémoire du début du vecteur, qui est généralement dans un registre d'adresse spécialisé, ou un registre non-SIMD. Avec des vecteurs de 8 octets, toute instruction d'accès mémoire de ce type va lire ou écrire des blocs de 8 octets. L'adresse de départ de ces blocs est soumise à des contraintes d'alignement sur les jeux d'instructions comme le SSE, le MMX, etc. La raison à cela est que gérer des accès non alignés en mémoire rend les circuits de lecture/écriture en mémoire plus complexes. En contrepartie, ces contraintes compliquent l'utilisation des instructions SIMD par le compilateur.
D'autres modes d'adressage des vecteurs permettent à une instruction de charger des données dispersées en mémoire pour les rassembler dans un vecteur. On peut notamment citer l'existence d'accès mémoires en stride et en scatter-gather.
L''''accès en stride''' regroupe des données séparées par un intervalle régulier d'adresses. Ce mode d'accès a besoin de l'adresse initiale, de celle du premier élément du vecteur, et de la distance entre deux données en mémoire. Il permet aux instructions de mieux gérer les tableaux de structures, ainsi que les tableaux multi-dimensionnels. Lorsqu'on utilise de tels tableaux, il arrive assez souvent que l'on n'accède qu'à des éléments tous séparés par une même distance. Par exemple, si on fait des calculs de géométrie dans l'espace, on peut très bien ne vouloir traiter que les coordonnées sur l'axe des x, sans accès sur l'axe des y ou des z. Les instructions d'accès mémoire en enjambées gèrent de tels cas efficacement.
Les processeurs SIMD incorporent aussi les accès en '''''scatter-gather'''''. De tels accès prennent en opérande un vecteur contenant des adresses mémoires, et lit/écrit chacune de ses adresses mémoire indépendamment. Les accès en ''scatter-Gather'' peuvent être vus comme une généralisation de l'adressage indirect à registre aux vecteurs, chaque élément du vecteur étant adressé via adressage indirect. Les accès en ''gather'' sont des lectures : elles prennent un vecteur d'adresse, lisent chaque adresse, et regroupent toutes les données lues dans un vecteur, dans un registre vectoriel. Les accès en ''scatter'' sont l'équivalent pour les écritures. Ils prennent un vecteur d'opérande pour les données à écrire, un vecteur pour les adresses.
[[File:Cuda4.png|centre|vignette|upright=2|Adressage en scatter-gather]]
Un autre version des accès en ''scatter-gather'' n'utilise pas des adresses mémoire, amis des indices. Le vecteur opérande ne contient pas des adresses, mais des indices qui sont combinés à une adresse de base pour calculer plusieurs adresses. Un tel mode d'adressage permet de faciliter l'implémentation de certaines structures de données, appelées des vecteurs de Liffe. Ils sont très utilisés pour gérer les matrices creuses, des matrices où une grande partie des éléments sont nuls et où, dans un souci d'optimisation, seuls les éléments non nuls de la matrice sont stockés en mémoire.
Lors d'une instruction de ''scatter'' ou de ''gather'', tous les accès mémoire n'ont pas lieu en même temps, certains accès mémoire seront terminés avant les autres. Les accès peuvent se faire dans le désordre et de nombreuses optimisations matérielle en profitent pour gagner en performance. Il existe cependant un cas où les accès mémoire doivent se faire dans un ordre bien précis, généralement du premier élément vers le dernier : lorsqu'une instruction ''scatter'' effectue plusieurs écritures à la même adresse. C'est parfaitement possible, et c'est le seul cas où il est intéressant de forcer un ordre pour les écritures.
En théorie, une instruction SIMD est un tout, c'est à dire qu'elle ne doit pas modifier les registres SIMD avant que tous les accès mémoire se soient terminés. Cependant, il est possible qu'une partie des accès mémoire déclenchent des défauts de page ou lèvent des exceptions matérielles, alors que les autres se sont terminés. En théorie, le processeur est censé se débarrasser du résultat des accès mémoire terminés. Mais ce serait un gachis de performance, ce qui fait que le processeur viole la règle voulant que les registres SIMD soient mis à jour en une seule fois à la fin de l'instruction de ''scatter-gather''. Dès qu'un accès mémoire se termine, il écrit son résultat dans le registre SIMD, à l'endroit adéquat.
Cependant, ce comportement pose un problème. Lorsqu'une exception survient, comme lors d'un défaut de page, le processeur exécute la routine d'interruption associée, puis redémarre l'instruction fautive, le second essai étant le bon. Ici, le processeur ne réexecute pas l'instruction à l'identique, mais seulement les accès non-terminés. Mais comment le processeur sait-il quelles accès mémoire redémarrer ? Il doit pour cela mémoriser quels sont les accès mémoire terminés. Il utilise pour cela un registre architectural appelé le '''masque de complétion'''. Le registre est forcément architectural car il doit être préservé lors d'un changement de contexte, l’exécution de l'exception matérielle forçant un changement de contexte.
===Exemple avec les processeurs x86===
[[File:PD-20060908-SSE3-01.svg|vignette|Chronologie des extensions x86 SIMD.]]
Après avoir vu la théorie sur les instructions SIMD, il est temps de voir un exemple concret : celui des instructions SIMD des processeurs x86, présents dans nos PC. Le jeu d'instruction des PC qui fonctionnent sous Windows est appelé le x86. C'est un jeu d'instructions particulièrement ancien, apparu en 1978. Depuis, des '''extensions x86''' ont ajouté des instructions au x86 de base. On peut citer par exemple les extensions MMX, SSE, SSE2, ''3dnow!'', etc.
Les premières instructions SIMD furent fournies par une extension x86 du nom de MMX, introduite par Intel en 1996 sur le processeur Pentium MMX. Le MMX a perduré durant quelques années avant d'être remplacé par les extensions SSE. La même année, AMD sorti d'expansion ''3DNow!'', qui ajoutait 21 instructions SIMD similaires à celles du MMX. Celui-ci fût suivi du ''3DNow!+'' quelques années plus tard. Le SSE fût ensuite décliné en plusieurs versions avant d'être "remplacé" par l'AVX. Une grande quantité de ces extensions x86 sont des ajouts d'instructions SIMD.
L’'''extension MMX''' ajoutait pas mal d'instructions SIMD assez basiques, essentiellement des instructions arithmétiques : addition, soustraction, opérations logiques, décalages, rotations, mise à zéro d'un registre, etc. La multiplication est aussi supportée, mais avec quelques petites subtilités, via l'instruction PMULLW. Les instructions MMX ne mettent pas le registre d'état à jour et ne préviennent pas en cas d'overflow ou d'underflow si ceux-ci arrivent (pour les instructions qui ne travaillent pas en arithmétique saturée).
Le MMX introduisait 8 registres vectoriels, du nom de MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Ils ne pouvaient contenir que des nombres entiers et faisaient 64 bits. Ils avaient cependant un léger défaut, qui a nui à l'adoption du MMX. Pour rappel, avant le MMX, les flottants étaient gérés par l'extension x87, qui définit 8 registres flottants de 80 bits. Et chaque registre MMX correspondait aux 64 bits de poids faible d'un registre flottant x87 ! Le système d'''alias'' de registres typique des CPU x86 a encore frappé ! En conséquence, il était impossible d'utiliser en même temps l'unité de calcul flottante et l'unité MMX. Par contre, sauvegarder les registres lors d'un changement de contexte, d'une interruption, ou d'un appel de fonction était très simple : la sauvegarde des registres de la FPU x87 suffisait.
[[File:MMX-FPU-Register.JPG|centre|vignette|upright=2|Registres MMX et FPU x87.]]
[[File:XMM registers.svg|droite|vignette|XMM registers]]
Dans les années 1999, une nouvelle extension SIMD fit son apparition sur les processeurs Intel Pentium 3 : le '''Streaming SIMD Extensions''', abrévié SSE. Ce SSE fut ensuite complété, et différentes versions virent le jour : le SSE2, SSE3, SSE4, etc. Cette extension fit apparaitre 8 nouveaux registres, les registres XMM. Sur les processeurs 64 bits, ces registres sont doublés et on en trouve donc 16. En plus de ces registres, on trouve aussi un registre d'état qui permet de contrôler le comportement des instructions SSE : celui contient des bits qui permettront de dire au processeur que les instructions doivent arrondir leurs calculs d'une certaine façon, etc. Ce registre n'est autre que le registre MXCSR. Chose étrange, seuls les 16 premiers bits de ce registre ont une utilité : les concepteurs du SSE ont surement préférés laisser un peu de marge au cas où.
La première version du SSE contenait assez peu d'instructions : seulement 70. Le SSE première version ne fournissait que des instructions pouvant manipuler des paquets contenant 4 nombres flottants de 32 bits (simple précision). Je ne vais pas toutes les lister, mais je peux quand-même dire qu'on trouve des instructions arithmétiques de base, avec pas mal d'opérations en plus : permutations, opérations arithmétiques complexes, autres. Petit détail : la multiplication est gérée plus simplement et l'on a pas besoin de s’embêter à faire mumuse avec plusieurs instructions différentes pour faire une simple multiplication comme avec le MMX.
On peut quand même signaler une chose : des instructions permettant de contrôler le cache firent leur apparition. On retrouve ainsi des instructions qui permettent d'écrire ou de lire le contenu d'un registre XMM en mémoire sans le copier dans le cache. Ces instructions permettent ainsi de garder le cache propre en évitant de copier inutilement des données dedans. On peut citer par exemple, les instructions MOVNTQ et MOVNTPS du SSE premiére version. On trouve aussi des instructions permettant de charger le contenu d'une portion de mémoire dans le cache, ce qui permet de contrôler son contenu. De telles instructions de prefetch permettent ainsi de charger à l'avance une donnée dont on aura besoin, permettant de supprimer pas mal de cache miss. Le SSE fournissait notamment les instructions PREFETCH0, PREFETCH1, PREFETCH2 et PREFETCHNTA. Autant vous dire qu'utiliser ces instructions peut donner lieu à de sacrés gains si on s'y prend correctement ! Il faut tout de même noter que le SSE n'est pas seul "jeu d'instruction" incorporant des instructions de contrôle du cache : certains jeux d'instruction POWER PC (je pense à l'Altivec) ont aussi cette particularité.
Avec le '''SSE2''', de nouvelles instructions furent ajoutés, permettant d'utiliser des nombres de 64, 16 et 8 bits dans chaque vecteur. Le SSE2 incorporait ainsi pas moins de 144 instructions différentes. Ce qui commençait à faire beaucoup.
Puis, vient le '''SSE3''', avec ses 13 instructions supplémentaires. Pas grand-chose à signaler, si ce n'est que des instructions permettant d'additionner ou de soustraire tous les éléments d'un paquet SSE ensemble, des instructions pour les nombres complexes, et plus intéressant : les deux instructions MWAIT et MONITOR qui permettent de paralléliser plus facilement des programmes.
Le '''SSE4''' fut un peu plus complexe et fut décliné lui-même en 2 versions. Le SSE4.1 introduit ainsi des opérations de calcul de moyenne, de copie conditionnelle de registre (un registre est copié dans un autre si le résultat d'une opération de comparaison précédente est vrai), de calcul de produits scalaire, de calcul du minimum ou du maximum de deux entiers, des calculs d'arrondis, et quelques autres. Avec le SSE4.2, le vice à été poussé jusqu'à incorporer des instructions de traitement de chaines de caractères.
[[File:AVX registers.svg|vignette|Registres AVX.]]
Avec l''''AVX (Advanced Vector eXtensions)''', on retrouve 16 registres d'une taille de 256 bits, nommés de YMM0 à YMM15 et dédiés aux instructions AVX. Ils sont partagés avec les registres XMM : les 128 bits de poids faible des registres YMM ne sont autres que les registres XMM. L'AVX complète le SSE et ses extensions, en rajoutant quelques instructions, et surtout en permettant de traiter des données de 256 bits.
Son principal atout face au SSE est que les instructions AVX permettent de préciser le registre de destination en plus des registres d'opérandes. Avec le SSE et le MMX, le résultat d'une instruction SIMD était écrit dans un des deux registres d'opérande manipulé par l'instruction. Il fallait sauvegarder son contenu si on en avait besoin plus tard, ce qui n'est plus nécessaire avec l'AVX.
==La performance des processeurs SIMD==
Il est intéressant de comparer la performance d'un processeur SIMD et celle d'un processeur normal, sans SIMD. Mais cet exercice est compliqué par le fait que les processeurs non-SIMD sont très nombreux : entre les CPU superscalaires, à émission dans l'ordre, exécution dans le désordre, ceux sans rien de tout cela, on a de quoi être perdu. Dans ce qui suit, nous allons comparer les processeurs SIMD avec deux types de processeurs : les processeurs à émission multiples et les processeurs basiques sans pipeline.
===Les processeurs SIMD comparés aux processeurs à émission multiple===
Le SIMD permet d'effectuer plusieurs calculs en parallèle, mais les architectures superscalaires en sont aussi capables, de même que les architectures VLIW. Prenons un processeur SIMD capables d'effectuer 16 calculs entiers/flottants en parallèle maximum, donc avec des vecteurs de 16 éléments. L'équivalent non-SIMD est : soit un processeur VLIW dont les ''bundles'' regroupent 16 instructions indépendantes, soit un processeur superscalaire capable d'émettre 16 instructions à la fois. Vous noterez que les trois processeurs en question disposent de 16 ALU pour ce faire.
Les trois architectures ont donc les mêmes performances dans un cas théorique idéal : les trois peuvent exécuter 16 calculs indépendants en même temps. Mais ce serait oublier que les architectures VLIW et superscalaires peuvent effectuer 16 calculs indépendants différents, là où le SIMD applique 16 calculs identiques sur des données différentes. Et ce cas est bien plus fréquent dans les codes généralistes. Le SIMD n'est donc utile que pour des codes spécialisés, où le parallélisme de donnée s'exprime d'une certaine manière.
Il faut cependant nuancer en tenant compte d'un point important : les architectures superscalaires doivent détecter les dépendances entre instructions avant d'éventuellement les exécuter en parallèle. Et cela a un cout en hardware qu'il faut payer. Avec le SIMD, on charge et décode une unique instruction, pas besoin de détecter les dépendances. Et un autre point est que les architectures superscalaires/VLIW exécutent réellement plusieurs instructions en parallèle, qui sont encodées telles quelles en mémoire. Alors qu'avec une architecture SIMD, les 8/16/32 calculs parallèles correspondent à une seule instruction. La densité de code est donc meilleure avec les architectures SIMD, si la situation s'y prête.
===Les processeurs SIMD comparés aux processeurs sans pipeline===
Comparé à un processeur sans pipeline, on s'attend à ce que la performance soit augmentée d'un facteur N, avec N le nombre maximal d'entiers/flottants que l'on peut mettre dans un vecteur. Si une architecture SIMD fait N calculs en parallèles, alors les performances sont censées être multipliées par N comparé à un processeur sans pipeline dont l'iPC maximale est de 1. Il laisse alors penser que plus les vecteurs sont longs, meilleur est le gain en performance. Il s'agit là d'un résultat basique, mais qui est assez approximatif, la vraie vie est différente.
Un premier problème est que ce résultat ne tient que si la mémoire RAM et les caches suivent. En effet, faire N calculs en parallèle demande de lire/écrire N fois plus de données. Les données sont souvent stockées dans des registres vectoriels, ce qui fait que la pression sur le banc de registre est assez importante. Mais cela signifie aussi que les instructions d'accès mémoire doivent lire/écrire N fois plus de données. Il faut alors ajouter des ports sur le cache, élargir le bus mémoire, augmenter la taille des lignes de cache, etc. Le débit binaire des caches et de la mémoire RAM deviennent rapidement des points limitants, qui réduisent le gain en performance des instructions SIMD. Et ne parlons pas de l'interaction avec la mémoire virtuelle : vu qu'on lit/écrit par paquets de données, cela signifie qu'on traverse une page mémoire plus vite, ce qui fait que les défauts de page sont lus fréquents. Un processeur SIMD a intérêt à être combiné à une RAM et des caches solides, très performants. C'est le cas sur les processeurs modernes, mais cela a pose des problèmes pour les processeurs vectoriels, et a mené à leur abandon progressif.
Mais passons outre ce problème et regardons la seconde raison qui font que ce résultat est naïf. N'oublions pas la loi d'Amdhal. Toutes les portions d'un programme ne sont pas accélérées par des instructions SIMD, il y a des portions dont les calculs ont des dépendances de données, d'autres qui ne sont pas parallélisables, etc. Une partie du programme est donc impossible à véctoriser (i.e optimiser pour le SIMD), une l'autre l'est. Pour simplifier les explications, on suppose qu'une instruction SIMD prend le même nombre de cycles que son équivalent non-SIMD. Par exemple, une addition SIMD prend le même temps qu'une addition normale. Dans ce cas, la portion non-vectorisable du code reste inchangée, alors que la portion vectorisable est divisée par N, par la largeur du vecteur. On retombe sur une formulation identique à la loi d'Amdhal sur le fond.
Une autre manière de compter le tout est d'utiliser la métrique de l'efficience SIMD. Elle est calculée en divisant deux grandeurs. La première est elle-même un rapport : c'est le gain obtenu en terme de nombre d'instructions exécutées. Prenons un programme qui exécute I instructions avant vectorisation, et qui en exécute I/X après. LE gain s'exprime comme suit :
: <math>G = \frac{I}{I/X} = X</math>
Maintenant, prenons ce gain est divisons-le par la largeur d'un vecteur. On obtient alors l'efficience SIMD :
: <math>\text{Efficience SIMD} = \frac{G}{N} = \frac{\frac{I}{I/X}}{N}</math>
Notons que le gain et la largeur SIMD ne sont égales que dans un cas bien précis : tout le code est vectorisable. Il s'agit donc d'une reformulation du gain de la loi d'Amdhal, mais dans laquelle le pourcentage de code série est caché.
Il est intéressant de regarder l'efficience SIMD quand on augmente la portion du code série, ainsi que la taille des vecteurs. Prenons un cas assez généreux, réaliste pour certaines applications très adaptées au SIMD : 1% du code est non-vectorisable, 99% profite du SIMD. L'efficience SIMD varie alors assez rapidement avec la taille des vecteurs. De 99% pour N=2, elle descend à 98% pour N=16, et tombe sous les 50% pou N=128. La conséquence est que les très grandes tailles de vecteurs ne sont pas vraiment utiles, le rendement est décroissant avec la taille des vecteurs.
==Les processeurs SIMD à registres généraux==
Le premier type de processeur SIMD que nous allons voir est celui des processeurs de type '''SWAR''' (''SIMD Within A Register''). Le terme SWAR est un terme polysémique dont le sens a beaucoup changé au fil du temps. Ici, nous allons l'utiliser dans la définition la plus stricte : celle où on effectue du SIMD dans les registres généraux du processeur, sans registres spécialisés. Il s'agit d'une forme de SIMD qui est aujourd'hui peu utilisée, mais qui a été la première forme de SIMD utilisée dans des processeurs grand public commerciaux.
L'idée est d'améliorer un petit peu un processeur normal, non-SIMD. Un processeur usuel contient des registres généraux de 32 à 64 bits, qui stockent chacun un opérande de même taille que le registre. D'anciens processeurs avaient des instructions pour effectuer des calculs simples, sur des opérandes plus courts. Par exemple, les processeurs x86 32 bits sont capables de faire des calculs sur 8, 16, ou 32 bits. Mais on ne pouvait placer qu'un seul opérande de 8, 16 ou 32 bits dans un registre général, ce qui est un léger gâchis. Le SWAR est une amélioration de cette technique qui vise à mieux utiliser les registres et l'ALU.
L'idée est de regrouper plusieurs opérandes de 8, 16, voire 32 bits, dans un seul registre général. De plus, on ajoute des instructions SIMD d'addition/soustraction/autres, qui lisent des opérandes 8/16/32 bits dans ces registres généraux. Par exemple, sur un processeur 32 bits, on peut ajouter une opération d'addition qui lit deux registres de 32 bits, pour récupérer quatre opérandes de 16 bits (deux par registre) et effectue deux additions 16 bits en même temps dans l'ALU. Et il y a la même chose pour d'autres opérations, comme la soustraction, la comparaison, etc.
Les instructions SIMD disponibles sur ces processeurs se limitent généralement à des additions, soustractions, comparaisons, mais guère plus. De plus, il s'agit d'instructions entières, il n'y a pas d'instructions SIMD flottantes. La raison à cela est très simple, mais nous l'expliquerons plus bas, dans la section sur l'implémentation. Disons simplement que de tells architectures visent une économie en circuit maximale. Elles implémentent les instructions SIMD en utilisant le moins de circuits possibles, ce qui fait qu'elles réutilisent les registres généraux et n'ont pas de registres SIMD séparés.
===Les extensions/jeux d'instructions de type SWAR===
Les premiers processeurs à intégrer du SWAR étaient les processeurs DEC Alpha, avec l'extension multimédia '''''Motion Video Extensions'''''. Les processeurs en question étaient les processeurs Alpha 21164PC (PCA56 and PCA57), Alpha 21264 (EV6) and Alpha 21364 (EV7). Les instructions SIMD disponibles étaient très simples et se résumaient à quelques comparaisons : trouver le maximum ou le minimum de deux opérandes de 8/16 bits, quelques instructions de permutation.
Les extensions '''''Multimedia Acceleration eXtensions''''' des processeurs Hewlett-Packard PA-RISC sont aussi de ce type, mais disposent de plus d'instructions SIMD. La première version, appelée MAX-1, était disponible sur les processeurs 32 bits de la marque, à partir du processeur PA-7100LC. Les instructions disponibles étaient des additions et soustractions d'opérandes 16 bit. Il y avait en tout trois instructions d'addition et trois pour la soustraction. En tout, il y avait :
* une instruction d'addition/soustraction non-signée utilisant l'arithmétique modulaire ;
* une instruction d'addition/soustraction signée en arithmétique modulaire ;
* une instruction d'addition/soustraction signée en arithmétique saturée.
Outre les additions/soustractions, il y avait aussi une instruction pour calculer la moyenne de deux opérandes 16 bits, ainsi qu'une addition fusionnée avec un décalage.
La seconde version, appelée MAX-2, ajouta des instructions d'additions fusionnées avec des décalages, mais aussi des instructions de permutation. De plus, cette version était disponible sur les processeurs 64 bits de la marque, ce qui fait que les registres étaient eux aussi de 64 bits. Ils pouvaient contenir deux fois plus d'opérandes 16 bits. Il n'y avait de gestion d'opérandes 32 bits pour les extensions SIMD.
===L'implémentation matérielle===
L'avantage de cette technique est la grande simplicité d'implémentation. On n'ajoute pas de registres SIMD séparés, ce qui a de nombreux avantages : économie de circuits car pas besoin d'un second banc de registre, pas besoin de sauvegarder des registres SIMD en plus lors d'un appel système/interruption, etc. L'implémentation a juste besoin de modifier le décodeur et l'unité de calcul. Le décodeur est modifié pour ajouter des instructions en plus, rien de spécifique au SWAR. Les modifications de l'unité de calcul sont spécifiques au SWAR et modifient la manière dont elle gère les retenues.
Une première solution est d'utiliser une ALU bit-slicée, ce qui est l'idéal pour les unités de calcul entières. Par exemple, pour une ALU 32 bits entière, on peut la découper en 4 unités de calcul 8 bits. Pour une opération SIMD avec 4 opérandes 8 bits, les quatre ALU fonctionnent en parallèle, il n'y a pas transmission des retenues. Pour les calculs SIMD avec des opérandes de 16 bits, on regroupe les ALU par paire et on propage les retenues à l'intérieur d'une paire, mais pas entre les paires. Enfin, pour un calcul non-SIMD, les opérandes de 32 bits, on propage les retenues normalement, d'une ALU 8 bits vers la suivante.
Une autre solution prend un additionneur 32/64 bits normal, mais mets à 0 les retenues. L'implémentation est très simple avec un additionneur à anticipation de retenues, où les retenues sont calculées en avance, avant de faire l'addition proprement dite, par un circuit d'anticipation de retenue. Il suffit alors d'ajouter un circuit de masquage en sortie du circuit d'anticipation de retenue, qui met à zéro les retenues adéquates. La gestion des débordements demande d'ajouter des circuits, ou du moins de modifier ceux déjà présents dans l'ALU. Typiquement, les circuits pour gérer l'arithmétique saturée sont un peu modifiés pour effectuer la mise à 1111...111 octet par octet et chaque octet est masqué si besoin.
[[File:ALU modifiée pour implémenter du SWAR.png|centre|vignette|upright=3|ALU modifiée pour implémenter du SWAR]]
Notons que la technique ne s'applique qu'aux additions, et aux opérations dérivées comme la soustraction et les comparaisons (ces dernières sont des soustractions déguisées). Mais les multiplications ou divisions ne peuvent pas s'implémenter simplement en modifiant des ALU existantes, du moins pas avec des modifications simples. Aussi, les processeurs qui utilisent cette technique se bornent aux additions et opérations dérivées, pas plus.
La gestion des opérations de permutation est quant à elle très simple : beaucoup de processeurs disposent déjà d'instructions de permutation d'octets, qui sont l'équivalent d'instructions SIMD de permutation pour les registres généraux. Nous en avions déjà parlé dans le chapitre sur le langage machine et l'assembleur, quand nous avions fait la liste des instructions les plus courantes. De telles instructions de permutation d'octet sont utiles pour gérer le boutisme ou effectuer quelques manipulations assez rares. Pas besoin de rajouter une ALU dédiée, celle-ci est déjà présente de base, du moins si les instructions de permutation d'octet sont déjà présentes.
==Les processeurs SIMD purs (''packed SIMD'') et SIMT==
Maintenant que nous avons vu les instructions SIMD, passons maintenant aux processeurs eux-même. Tous les processeurs que nous allons voir dans ce qui suit supportent des instructions SIMD. Mais leur implémentation matérielle, leur micro-architecture, n'est pas la même. Ils ont aussi quelques différences en termes de jeu d'instruction, mais qui sont fortement liées à l'implémentation matérielle.
Nous allons ici voir les '''processeurs SIMD purs''', aussi dits de type ''Packed SIMD''. Ils implémentent des instructions SIMD sans rien de plus, juste le strict minimum : pas de prédication, pas de vecteurs de taille variable, presque pas d’instructions horizontales, architecture de type LOAD-STORE. De tels processeurs utilisent plusieurs ALU entières/flottantes qui travaillent en parallèle. De plus, ils disposent de registres SIMD séparés des registres généraux. Voyons ces points immédiatement.
===Les instructions SIMD verticales : plusieurs ALU travaillant en parallèle===
[[File:SIMD2.svg|vignette|Parallélisme de données au niveau de l'unité de calcul. Celle-ci contient plusieurs circuits indépendants qui appliquent la même opération sur des données différentes.]]
Sur un processeur SIMD pur, les vecteurs sont stockés dans des registres séparés des registres généraux. Les vecteurs sont beaucoup plus longs qu'un entier/flottant normal, ils peuvent en regrouper plusieurs. Par exemple, sur un processeur 32 bits, qui gère donc des entiers de 32 bits, les vecteurs peuvent faire 128 ou 256 bits. Un vecteur contient donc plusieurs entiers ou flottants de taille maximale. On n'est pas dans le cas précédent, où registres généraux et SIMD sont les mêmes, ils sont séparés et n'ont pas la même taille.
Les calculs sont effectués en parallèle dans des ALU séparées. Une unité de calcul SIMD contient plusieurs additionneurs/multiplieurs séparés. Par exemple, pour additionner deux vecteurs contenant chacun 16 entiers/flottants, il faut utilise 16 additionneurs entiers et 16 additionneurs flottants. Dans le cas général, une ALU SIMD est composée de plusieurs ALU entières et flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. Notons que les ALU travaillent en parallèle, elles font des calculs indépendants.
Notons que les additionneurs dans chaque ALU doivent pouvoir être configurés de manière à gérer des tailles de données différentes. Par exemple, si on prend un vecteur simple, qui peut contenir soit 32 entiers de 16 bits, soit 16 entiers 32 bits, soit 8 entiers 64 bits, alors on doit utiliser 8 additionneurs, mais chacun d'entre eux doit pouvoir être reconfiguré de manière à ne pas propager les retenues au-delà des premiers 16 ou 32 bits.
Un défaut de cette organisation est que le cout en circuits est loin d'être négligeable. Il faut dupliquer des unités de calcul et les coller en rajoutant des circuits, cela utilise beaucoup de transistors. Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Retenez cependant : l'usage de plusieurs ALU travaillant en parallèle a plusieurs défauts. Premièrement, cela implique des vecteurs de taille fixe, du fait que le nombre d'ALU travaillant en parallèle est fixe. Deuxièmement, des difficultés d'implémentation concernant les instructions de réduction. Détaillons ce second point !
Une autre possibilité est d'utiliser moins d'unités de calcul qu'il n'y a d'éléments dans un vecteur. Par exemple, pour des vecteurs contenant 32 flottants, il se peut qu'il n'y ait que 16 ALU. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale. L'avantage est que l'ensemble des calculs se fait en une seule instruction machine. L'implémentation est généralement la suivante : le processeur décode une instruction SIMD en deux micro-instructions SIMD plus courtes. Par exemple, pour une instruction SIMD de 32 éléments, elle sera décodée en deux instructions SIMD de 16 éléments, chacune étant exécutée sur l'ALU.
Il est possible de réduire fortement l'impact en performance en doublant la fréquence de l'ALU. Par exemple, pour des vecteurs contenant 32 flottants, le processeur incorpore 16 ALU, mais celles-ci fonctionne à une fréquence double de celle du processeur. L'usage d'une ALU à double fréquence est une technique qui a été utilisée sur le Pentium 4 et quelques processeurs, pour des instructions non-SIMD. Mais pour les processeurs SIMD, elle s'applique à la perfection. L'implémentation est la même que précédemment, sauf que la fenêtre d'instruction et la logique d'émission fonctionnent à double fréquence. Pour limiter la casse, il est préférable d'utiliser une fenêtre d'instruction séparée pour les instructions SIMD, qui sera seule à fonctionner à double fréquence.
===Les instructions horizontales : une ALU simple séparée===
Les instructions horizontales posent des difficultés d’implémentation. Elles ne peuvent pas s'implémenter en utilisant plusieurs ALU travaillant en parallèle, contrairement aux instructions verticales, ce qui fait qu'elles utilisent généralement des unités de calcul spécialisées. Et c'est là que la différence entre instructions de manipulation de vecteurs et instructions de réduction vient encore une fois poser problème. Les instructions de manipulation de vecteur sont relativement simples à implémenter : elles ont juste besoin d'une ALU spécialisée, relativement simple, composée de beaucoup de multiplexeurs à configurer convenablement. Mais pour les instructions de réduction, c'est autre chose !
L'implémentation des instructions de réduction est possible mais a un cout en circuits assez prohibitif. Pour additionner N entiers, il faut utiliser un additionneur multi-opérande qui prend beaucoup de circuits et dont le temps de calcul est proche de celui d'une multiplication (très lent). Trouver le maximum et le minimum d'un nombre demandent de faire la même chose avec des comparateurs. Le tout a un cout en circuit non-négligeable, pour accélérer des opérations assez peu fréquentes. Elles sont surtout utilisées pour trouver la somme ou le maximum/minimum d'un tableau, opération séquentielle par nature, avec peu parallélisme de données, peu courante.
Une autre solution utilise une unité SIMD normale, mais intègre un système de contournement, de ''bypass'', qui relierait l'entrée d'une ALU à la sortie d'une autre. Mais le cout lié aux interconnexions est très important : on doit relier chaque ALU à toutes les autres dans le pire des cas, on peut optimiser le tout et réduire un peu la quantité d'interconnexions, mais le cout reste important. Au final, on gagne en circuits ce qu'on perd en interconnexions. Le choix entre les deux solutions est loin d'être facile et dépend du processeur, de la technologie utilisée, du budget en transistors disponibles, etc.
Les processeurs SIMD purs résolvent ce problème assez simplement : ils n'implémentent pas d'instructions de réduction. Par contre, ils implémentent systématiquement des instructions de manipulation de vecteur, comme des permutations ou autres. La raison est que l'on peut émuler les instructions de réduction en utilisant des instructions SIMD de manipulation de vecteur couplées à des additions/multiplications SIMD verticales. Il s'agit donc d'un compromis entre cout en circuits et performance finale. Niveau circuits, on a une unité SIMD avec plusieurs ALU de calcul en parallèle, une unité de manipulation de vecteur séparée assez simple et au cout en circuits raisonnable. Les performances pour les opérations de réduction sont acceptables, le cout en performance est relativement modéré.
===L'implémentation des LOAD-STORE à des données consécutives===
Les architectures SIMD pures sont des architectures de type LOAD-STORE, ce qui veut dire que les instructions SIMD ne peuvent que lire ou écrire dans les registres vectoriels, pas en mémoire ou ailleurs. Les transferts entre registres SIMD et mémoire sont le fait d'instructions LOAD et STORE spécialisées, qui transfèrent des vecteurs entre RAM et registres vectoriels. Les autres architectures n'ont pas nécessairement ce genre de restrictions, mais laissons cela pour plus tard.
Implémenter les instructions LOAD/STORE pour des vecteurs dont les données sont consécutives en mémoire n'est pas très compliqué. Il suffit juste d'élargir le port de lecture/écriture du cache L1 de données, pour pouvoir lire/écrire un vecteur entier. Rien de bien compliqué, il suffit juste d'ajouter des interconnexions et d'ajouter des multiplexeurs. Du moins, c'est le cas tant que les vecteurs sont plus petits qu'une ligne de cache, ce qui est souvent le cas en pratique. Dans le cas très rare où les vecteurs sont plus longs qu'une ligne de cache, les LOAD/STORE doivent se faire en deux accès dans le cache, ce qui demande d'ajouter du matériel pour contrôler la situation.
Précisons cependant qu'il s'agit là d'un cas idéal, où les vecteurs sont correctement alignés en mémoire. En effet, il se peut qu'un vecteur soit à cheval sur deux lignes de cache, alors qu'il est plus petit qu'une ligne de cache. Il suffit pour cela qu'il soit placé à une adresse adéquate. Par exemple, prenons un vecteur de 16 octets et des lignes de cache de 32 octets. Si le vecteur démarre à l'adresse 24, alors il sera à cheval sur deux lignes de cache. Il faut donc intégrer des circuits pour gérer cette situation. Pour cela, il suffit d'ajouter un registre pour stocker la première de ligne lue, puis un circuit qui combine les deux lignes de cache pour donner le vecteur final.
===L'implémentation des LOAD-STORE en ''scatter-gather''===
L'implémentation des accès en ''scatter-gather'' est plus compliquée. Un accès en ''scatter-gather'' demande de faire des calculs d'adresse, suivis par un ensemble de lectures/écritures, suivis par un regroupement des données lues dans un vecteur. Le calcul d'adresse peut se faire en parallèle dans une unité de calcul SIMD, dans plusieurs unités de calcul d'adresse en parallèle. Par contre, les lectures/écritures ne le peuvent pas. Le cache n'a pas assez de ports de lecture/écriture pour lire/écrire N données. Il a quelques ports, ce qui lui permet de lire/écrire 3/4 données grand maximum, soit moins que ce qui est nécessaire pour faire un accès en ''scatter-gather'' en une fois. Les lectures/écritures sont donc effectuées en plusieurs fois. Pour une opération de ''Gather'', il faut aussi regrouper les données lues dans un vecteur.
Sur les caches des processeurs à haute performance, le calcul d'adresse est intégré dans le décodeur. Ce sont des caches adressés par somme, que nous avions abordé dans le chapitre sur les mémoires caches. Avec de telles caches, on ne peut pas utiliser une unité de calcul d'adresse SIMD, on ne peut calculer qu'autant d'adresse qu'il y a de ports sur le cache.
Les accès en ''scatter-gather'' regroupent plusieurs accès mémoire, qui peuvent être effectués dans le désordre. La seule exception est celle où un ''scatter'' effectue plusieurs écritures à la même adresse, où les deux écritures doivent se faire dans l'ordre allant du premier au dernier élément du vecteur d'adresse. Les processeurs SIMD profitent de cette absence d'ordre pour effectuer les accès dans un ordre le plus optimisé possible.
Par exemple, imaginons le cas où une instruction ''gather'' lise des éléments placés dans deux lignes de cache uniquement, mais dans le désordre en passant sans cesse d'une ligne de cache à l'autre. Il est alors possible de faire tous les accès dans la première ligne de cache en premier, avant de faire ceux allant dans l'autre. Une telle optimisation marche aussi bien pour les lectures que les écritures, pour les ''gather'' que pour les ''scatter''. Elle peut aussi être adaptée pour plus de deux lignes de cache. Elle porte le nom de '''''memory coalescing'''''.
Une autre optimisation possible est de détecter le cas où un accès en ''scatter-gather'' accède à des données consécutives. Il suffit pour cela que les indices/adresses du vecteur opérande soient consécutives, et cela arrive plus souvent qu'on ne le pense. Dans ce cas, l'accès est remplacé par une instruction LOAD/STORE SIMD normale, beaucoup plus simple et plus rapide. Les deux optimisations demandent que les adresses soient calculées avant de faire l'accès mémoire. Les adresses calculées sont alors comparées entre elles, par un réseau de comparateurs assez complexe.
Un point important de l'implémentation des instructions ''gather'' est qu'une donnée chargée doit être insérée au bon endroit dans le vecteur destination. Et pareil pour les ''scatter'' : il faut lire la bonne donnée au bon moment. Si on prend une ligne de cache complète, il faut lire plusieurs élèments en même temps et les envoyer au registre de destination au bon endroit. Il faut pour cela tout un réseau de multiplexeurs assez complexe pour faire ce travail. Il y a le même genre de réseau pour les instructions ''scatter'', mais qui va dans l'autre sens : du registre vers la ligne de cache.
==Les processeurs SIMD avec prédication==
Un obstacle très gênant à la vectorisation est la présence de branchements conditionnels dans les boucles à vectoriser. Si une boucle contient des branchements conditionnels, elle ne peut pas être vectorisée facilement : il est impossible de zapper certains éléments d'un vecteur suivant une condition. Il n'est pas possible d'effectuer une instruction SIMD seulement sur certains éléments d'un vecteur et d'ignorer les autres, en fonction du résultat d'un branchement conditionnel. Tout le vecteur y passe, ou le vecteur est épargné, mais la condition intermédiaire n'est pas possible avec du SIMD simple.
Pour résoudre ce problème, les processeurs SIMD incorporent diverses techniques pour implémenter ou éviter les branchements conditionnels le plus possible. La plus simple est l'incorporation de certaines instructions SIMD simples, comme la valeur absolue, le calcul du minimum/maximum, etc. Ces opérations sont généralement émulées en utilisant des branchements, avec quelques conditions simples. Incorporer ces instructions permet ainsi de faire disparaitre une partie des branchements du code. Elle ne paye pas de mine, mais elle a un résultat pas négligeable pour le rendu graphique, certaines applications de calcul scientifiques, et quelques autres. Son cout en transistors est très variable, il faut rajouter des circuits dans le séquenceur, l'unité de calcul SIMD est assez peu modifiée (il faut juste rajouter des multiplexeurs).
Le vrai problème est que cette solution laisse énormément de branchements dans le code. Et qu'il faut donc trouver une solution plus générale, capable de réduire drastiquement les branchements. Pour cela, on réutilise une technique vue dans les chapitre sur l'assembleur et le langage machine : la prédication.
===Les instructions à prédicats===
Pour optimiser le cas général, il est possible d'utiliser des instructions à prédicats adaptées pour fonctionner sur des vecteurs. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteurs sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
La prédication peut aussi être utilisée pour simuler des vecteurs de taille variable. Tous les vecteurs ont une taille fixe, mais on peut ne pas faire de calculs sur la fin d'un vecteur, ce qui fait que tout se passe comme si le vecteur était plus petit. Ce n'est pas sa seule utilisation, mais la prédication permet de simuler ce comportement. Néanmoins, cela n'en fait pas un processeur vectoriel, capable de traiter des vecteurs de taille variable : il n'y a pas de moyen de préciser explicitement la taille des vecteurs à traiter.
Tout cela demande plusieurs modifications du jeu d'instruction : ajouter des instructions de comparaison SIMD, ajouter de quoi faire la prédication.
===Le calcul des masques et le registre de masque===
La prédication demande que le résultat d'une comparaison soit stocké dans un registre à prédicat ou dans le registre d'état. Pour les processeurs SIMD, il y a cependant une grosse adaptation à faire concernant le fonctionnement des comparaisons et le stockage des résultats.
Premièrement, les instructions de comparaison SIMD comparent une paire de deux éléments à la même place dans deux vecteurs et fournissent un résultat d'un bit pour chaque paire. Le résultat est donc un ensemble de bits, qui sont regroupées dans un vecteur spécialisé.
Deuxièmement, reste à savoir que faire de ce vecteur. Pour cela, deux solutions sont possibles. Avec la première, le vecteur est stocké dans un registre vectoriel/SIMD comme les autres, il est traité comme n'importe quel autre vecteur. Avec la seconde solution, il reçoit un registre dédié, qui est un mix entre registre d'état et registre à prédicat, appelé un '''registre de masque''' (''Vector Mask Register''). Il stocke un bit pour chaque donnée présente dans le vecteur à traiter, qui indique s'il faut ignorer la donnée ou non. L'instruction SIMD suivante fait usage de ce masque pour savoir s'il faut faire l'opération pour chaque élément du vecteur.
[[File:Vector mask register.png|centre|vignette|upright=2|Vector mask register]]
Il peut y avoir un ou plusieurs registres de masque. S'il y en a plusieurs, ils sont généralement nommés, sur le modèle des registres à prédicats. L'application d'un masque demande de fournir le nom du registre de masque à utiliser, ils adressés explicitement dans les instructions. S'il n'y en a qu'un seul, il est possible de le pas le nommer et de faire en sorte qu'il soit adressé implicitement par les instructions qui en ont besoin. On parle alors de '''masquage implicite'''. Le masquage implicite est très utilisé sur les cartes graphiques, qui sont actuellement des architectures SIMD, comme nous le verrons plus bas.
Un gros problème du masquage implicite est qu'il introduit des dépendances implicites entre instructions, qui ne sont pas explicites quand on regarder uniquement les registres manipulés par une instruction, qui perturbent le renommage de registres. Rien d'insurmontable, mais cela peut poser des problèmes de performance et la résolution de ce problème a un cout en hardware. Mais ce n'est un problème que sur les architectures à exécution dans le désordre. Les architectures à émission dans l'ordre ne sont pas concernées, et c'est notamment le cas sur les GPU et les cartes graphiques modernes.
Diverses optimisations sont possible avec un registre de masque. La première est de ne pas exécuter d'instruction si tous les bits du masque sont à 0. Dans ce cas, l'instruction SIMD n'a pas de calculs à faire, vu que tous les résultats sont masqués. Il est alors possible de ne pas exécuter l'instruction SIMD et de passer directement à la suivante. L'implémentation matérielle est très simple : une porte NOR qui détermine si le registre de masque contient la valeur 0. La sortie de cette porte NOR est envoyée au séquenceur d'instruction. Le décodeur est alors conçu pour zapper l'instruction si le registre de masque contient un zéro.
===Les accès mémoire en ''scatter-gather'' avec prédication===
Un problème avec les accès mémoire que certains accès peuvent déclencher des défauts de page ou d'autres formes d'exceptions (erreurs de protection mémoire, autre). Si l'accès mémoire se fait sans prédication, ces exceptions sont gérées dans l'ordre des accès mémoire, en commençant par le début du vecteur, ce qui rend l'implémentation assez facile. Mais avec la prédiction, il se peut qu'un accès masqué et censé être ignoré déclenche une exception matérielle. Et le résultat dépend de l'implémentation.
Une première solution est d'effectuer l'accès mémoire, et de masquer ensuite les éléments à ignorer. Mais dans ce cas, les éléments masqués peuvent déclencher des exceptions, qui ne sont pas censées se produire. Une autre solution est de déterminer quelles sont les adresses qu'il faut lire ou écrire et ne pas faire les accès mémoire masqués. Une autre solution effectue tous les accès avant de savoir quels sont ceux à masquer, mais de ne pas déclencher d'exception avant qu'on sache quels sont les accès masqués. Une fois le masquage des accès effectués, le processeur exécute les exceptions adéquates. Il s'agit d'un comportement de masquage d'exception, très utilisé.
L'implémentation est assez simple pour les accès mémoire contiguës, mais plus compliqué pour les accès en ''stride'' ou en ''scatter-gather''. Pour les accès contiguës, les seules exceptions pouvant survenir sont celles ayant lieu quand on passe d'une page mémoire à la suivante (au sens pagination, mémoire virtuelle). Et le hardware pour détecter un débordement de page est assez simple. Mais pour les accès en ''stride'' ou en ''scatter-gather'' demandent qu'oin vérifie les exceptions pour chaque élément du vecteur.
===L'implémentation matérielle===
L'implémentation matérielle est assez proche des processeurs SIMD purs. On retrouve plusieurs unités de calcul travaillant en parallèle, avec quelques circuits ajoutés pour gérer la prédication. Le cout supplémentaire en circuits est acceptable, les gains en performance associés sont très intéressants. Les circuits en plus, pour gérer la prédication, sont similaires à ceux utilisés pour la prédication sur les architectures non-SIMD, mais sont dupliqués.
La seule innovation architecturale est l'implémentation du registre de masque. Tous les processeurs n'en utilisent pas, mais c'est la solution la plus performante. En effet, ce registre de masque est un peu l'équivalent du registre d'état pour les instructions SIMD, bien qu'il soit techniquement une concaténation de registres à prédicat non-nommés. Ils ont pour avantage de libérer des registres SIMD normaux tout en ayant un cout en matériel très limité. Un autre point est que pour gérer les masques, les instructions SIMD à prédicat doivent lire le masque. Sans registre de masque, en utilisant un registre SIMD normal, il faut rajouter un troisième port de lecture sur le banc de registre pour récupérer le masque, ce qui est couteux en matériel. Pas besoin avec un registre de masque, qui n'est connecté qu'à l'ALU, sur le même modèle que le registre d'état.
Plus haut, on a vu que l'implémentation de l'ALU SIMD peut se faire en utilisant des moins d'ALU que prévu. Par exemple, pour des vecteurs de 32 éléments, on peut utiliser 16 ALUs (allant à double fréquence ou non). Une instruction SIMD est alors décodée en plusieurs micro-instructions SIMD consécutives, chacune traitant une partie d'un vecteur. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. L'avantage est que cela se marie bien avec l'abandon des opérations pour les masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit vérifier que les 8 bits de poids faible ainsi que de poids fort du registre de masque ne soient pas individuellement à zéro. Cela se fait en dupliquant la porte NOR évoquer plus haut , l'une pour la partie basse du registre et la seconde pour la partie haute.
==La prédication pour les structures de contrôle imbriquées==
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
===La pile de masques===
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les maques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au somment de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalent de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la techniques des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémenté en entrant dans la structure de contrôle ne sont pas décrémenté en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
==Les processeurs vectoriels==
Les '''processeurs vectoriels''' sont les ancêtres des processeurs à instruction SIMD. Bien qu'ils soient arrivés en premier, ils incorporent diverses techniques que de simples instructions SIMD n'ont pas forcément. C'est paradoxal, mais c'est ainsi.
La première différence se manifeste au niveau du jeu d'instruction. Les processeurs vectoriels sont capables de traiter des vecteurs de taille variable, alors que les instructions SIMD usuelles ne gèrent que des vecteurs de taille fixe. Par taille variable, on veut dire que le processeur gère nativement des vecteurs d'une taille allant de 1 à la taille maximale d'un vecteur. La taille maximale d'un vecteur est celle permise par la taille des registres vectoriels, il s'agit de la taille d'un vecteur SIMD équivalent sur les autres processeurs SIMD.
La seconde différence est une différence en termes de micro-architecture. Un processeur SIMD actuel dispose d'une unité de calcul très élaborée, capable de faire plusieurs calculs en parallèle, au moins un pour chaque élément du vecteur. Par exemple, si un vecteur contient 16 entiers, l'ALU SIMD doit contenir au moins 16 additionneurs entiers. Mais sur un processeur vectoriel, ce n'est pas le cas : l'unité de calcul est en réalité une unité de calcul entière/flottante normale, mais qui a la particularité d'être pipelinée. Les calculs sont donc effectués en parallèle, mais d'une manière totalement différente.
La plupart de ces différences s'expliquent par le fait que les anciens processeurs vectoriels devaient faire avec des limitations en termes de circuits. Ils avaient peu de transistors à leur disposition, ce qui fait que leur unité de calcul devait être la plus simple possible. D'où l'utilisation d'une unité de calcul simple, mais utilisée de manière pipelinée. De plus, les processeurs vectoriels ne possèdent aucune mémoire cache pour les données et se contentent juste de caches d'instruction. De tels caches sont généralement peu utiles quand on manipule des tableaux, chose quasiment systématique quand on travaille avec du parallélisme de données.
===Une unité de calcul pipelinée===
La différence entre processeur vectoriel et SIMD tient dans la façon dont sont traités les vecteurs : les instructions SIMD traitent chaque élément en parallèle, alors que les processeurs vectoriels pipelinent ces calculs ! Par pipeliner, on veut dire que l’exécution de chaque instruction est découpée en plusieurs étapes indépendantes. Au lieu d'attendre la fin de l’exécution d'une opération avant de passer à la suivante, on peut commencer le traitement d'une nouvelle donnée sans avoir à attendre que l'ancienne soit terminée.
[[File:Pipeline.png|centre|vignette|upright=2|Pipeline vectoriel.]]
Pour donner un exemple, on peut donner l'exemple d'une multiplication flottante effectuée entre deux registres. Son exécution peut être décomposée en plusieurs étapes.
Par exemple, on peut avoir 3 étapes :
* une première étape E qui va additionner les exposants et gérer les diverses exceptions ;
* une étape M qui va multiplier les mantisses ;
* et enfin une étape A qui va arrondir le résultat.
L’exécution de notre opération flottante sur un vecteur donnerait donc quelque chose dans le genre, où chaque ligne correspond au traitement d'un nouvel élément dans un vecteur, dans un paquet SIMD.
[[File:Multiplication vectorielle - pipeline.png|centre|vignette|upright=2|Multiplication vectorielle - pipeline]]
: Certains processeurs vectoriels utilisent plusieurs ALU pipelinées pour accélérer les calculs. On peut les voir comme des intermédiaires entre processeur vectoriels et SIMD SWAR.
Avec une unité de calcul pipelinée découpée en N étages, on peut gérer N données simultanées : autant qu'il y a d'étapes différentes. Mais ce nombre maximal de données met un certain temps avant d'être atteint. L'unité de calcul met du temps avant d'arriver à son régime de croisière. Durant ce temps, elle n'a pas commencé à traiter suffisamment d’éléments pour que toutes les étapes soient occupées. Ce temps de démarrage est strictement égal du nombre d'étapes nécessaires pour effectuer une instruction. La même chose arrive vers la fin du vecteur, quand il ne reste plus suffisamment d’éléments à traiter pour remplir toutes les étapes.
[[File:Startup and dead time vector pipeline.png|centre|vignette|upright=2|Startup and dead time vector pipeline]]
Pour amortir ces temps de démarrage et de fin, certains processeurs démarrent une nouvelle instruction sans attendre la fin de la précédente : deux instructions peuvent se chevaucher pour remplir les vides. Par contre, il peut y avoir des problèmes lorsque deux instructions qui se suivent manipulent le même vecteur. Il faut que la première instruction ait fini de manipuler un élément avant que la suivante ne veuille commencer à le modifier. Pour cela, il faut avoir des vecteurs suffisamment qui contiennent plus d’éléments qu'il n'y a d'étapes pour effectuer notre instruction.
===La technique du ''chaining''===
La technique du pipeline peut encore être améliorée dans certains cas particuliers. L'idée est simplement d'ajouter la technique du contournement (''bypass'') à l'unité de calcul. Pour rappel, le contournement connecte la sortie de l'ALU à une de ses entrées, ce qui permet au résultat d'un calcul d'être réutilisable au cycle d’horloge suivant. L'usage du contournement sur les processeurs vectoriels porte le nom de '''''Vector Chaining'''''. Un processeur implémentant le ''chaining''/''bypass'' a toutes ses unités de calcul reliées entre elles : la sortie d'une unité est reliée aux entrées de toutes les autres.
Pour comprendre à quoi peut servir cette technique, on peut citer deux grands exemples principaux. Le premier avantage est que l'implémentation des instructions SIMD horizontales est beaucoup plus simple. C'est pour cela que les processeurs vectoriels intègrent souvent des instructions horizontales, et notamment des instructions arithmétiques horizontales. Le contraste avec les processeurs SIMD récents, qui utilisent plusieurs ALU travaillant en parallèle, est frappant. Ces derniers n’intègrent pas d'instructions horizontales arithmétiques, les seules opérations horizontales supportées sont généralement des permutations ou des instructions ''compress/expand'', guère plus.
Maintenant, introduisons une autre possibilité par un exemple. Dans cet exemple, on suppose que le processeur dispose d'une ALU pour l'addition et d'une autre pour la multiplication. Imaginons que l'on ait trois vecteurs nommés A, B et C. Pour chaque énième élément de ces paquets, je souhaite effectuer le calcul <math>A_n + B_n \times C_n</math>. En théorie, il faudrait faire d'abord la multiplication, stocker le résultat temporaire de la multiplication dans un registre vectoriel, puis faire l'addition. Mais en rusant un peu, on peut utiliser le pipeline plus efficacement. Une fois que le premier élément de la multiplication du premier vecteur est connu, pourquoi ne pas démarrer l'addition immédiatement après, et continuer la multiplication en parallèle ? Après tout, les deux calculs ont lieu dans des ALUs séparés. Et le contournement permet d'alimenter l'ALU d'addition avec la sortie du circuit multiplieur.
[[File:Vector chaining.png|centre|vignette|upright=2|Vector chaining]]
===La gestion des vecteurs de taille variable===
L'usage d'une unité de calcul pipelinée fait que la taille des vecteurs n'est pas forcément fixe. Autant les processeurs SIMD non-vectoriels disposent d'un nombre fixe d'unités de calcul, et peuvent donc gérer des vecteurs de taille fixe, autant ce n'est pas le cas des processeurs vectoriels. Une unité de calcul pipelinée à 16 étage peut utiliser les trois premiers pour traiter un vecteur de 3 élements, les 5 suivants pour un second vecteur de 5, et le reste pour un troisième vecteur. Reste que la gestion des vecteurs de taille variable doit être gérée au niveau du séquenceur, mais surtout : doit être permise au niveau du jeu d'instruction.
Pour cela, il faut ajouter au jeu d'instruction de quoi indiquer la taille du vecteur en cours de traitement. Et c'est le rôle d'un registre spécialisé, le '''Vector Length Register''', que de gérer les vecteurs de taille variable. Le ''Vector Length Register'' est un registre qui indique combien d’éléments on doit traiter dans un vecteur. On peut ainsi dire au processeur : je veux que tu ne traites que les 40 premiers éléments présents d'un paquet. Ils sont utilisés pour gérer des tableaux dont la taille n'est pas un multiple d'un vecteur. Quand on arrive à la fin d'un tableau, il suffit de configurer le ''Vector Length Register'' pour ne traiter que ce qu'il faut.
[[File:Vector length register.png|centre|vignette|upright=2|Vector length register.]]
Il facilite l'usage du '''déroulage de boucle''', une optimisation qui vise à réduire le nombre d'itérations d'une boucle en dupliquant son corps en plusieurs exemplaires. Le compilateur réplique le corps de la boucle (les instructions à répéter) en plusieurs exemplaires dans cette boucle, avant de corriger le nombre d'itérations de la boucle. Par exemple, prenons cette boucle, écrite dans le langage C :
<syntaxhighlight lang="c">
int i;
for (i = 0; i < 100; ++i)
{
a[i] = b[i] * 7 ;
}
</syntaxhighlight>
Celle-ci peut être déroulée comme suit :
<syntaxhighlight lang="c">
int i;
for (i = 0; i < 100; i+=4)
{
a[i] = b[i] * 7 ;
a[i+1] = b[i+1] * 7 ;
a[i+2] = b[i+2] * 7 ;
a[i+3] = b[i+3] * 7 ;
}
</syntaxhighlight>
Les instructions vectorielles permettent de traiter plusieurs éléments à la fois, ce qui fait que plusieurs tours de boucles peuvent être rassemblés en une seule instruction SIMD. Le déroulage de boucles permet d'exposer des situations où ce regroupement est possible. Dans notre exemple, si jamais notre processeur dispose d'une instruction de multiplication capable de traiter 4 éléments du tableau a ou b en une seule fois, la boucle déroulée peut être vectorisée assez simplement en utilisant une multiplication vectorielle (que nous noterons vec_mul).
<syntaxhighlight lang="c">
int i;
for (i = 0; i < 100; i+=4)
{
vec_a[i] = vec_mul ( vec_b[i] , 7 ) ;
}
</syntaxhighlight>
Le déroulage de boucles n'est toutefois pas une optimisation valable pour toutes les boucles. Reprenons l'exemple de la boucle vue plus haut. Si jamais le tableau à manipuler a un nombre d’éléments qui n'est pas multiple de 4, la boucle ne pourra être vectorisée, vu que la multiplication vectorielle ne peut traiter que 4 éléments à la fois. Pour ce faire, les compilateurs utilisent généralement deux boucles : une qui traite les éléments du tableau avec des instructions SIMD, et une autre qui traite les éléments restants avec des instructions non vectorielles. Cette transformation s'appelle le '''strip-mining'''.
Par exemple, si je veux parcourir un tableau de taille fixe contenant 102 éléments, je devrais avoir une boucle comme celle-ci :
<syntaxhighlight lang="c">
int i;
for (i = 0; i < 100; i+=4)
{
a[i] = b[i] * 7 ;
a[i+1] = b[i+1] * 7 ;
a[i+2] = b[i+2] * 7 ;
a[i+3] = b[i+3] * 7 ;
}
for (i = 100; i < 102; ++i)
{
a[i] = b[i] * 7 ;
}
</syntaxhighlight>
Les processeurs vectoriels utilisent le ''Vector Length Register'' pour éviter d'utiliser le strip-mining. Avec ce registre, il est possible de demander aux instructions vectorielles de ne traiter que les n premiers éléments d'un vecteur : il suffit de placer la valeur n dans le ''Vector Length Register''. Évidemment, n doit être inférieur au nombre d’éléments maximal du vecteur. Avec ce registre, on n'a pas besoin d'une seconde boucle pour traiter les éléments restants, et une simple instruction vectorielle peut suffire.
L'avantage principal est que cela réduit la portion de code non-parallélisable, donc augmente l'efficience SIMD. Mais l'intérêt n'est pas qu'une question de code. Il faut aussi prendre en compte que cela permet d'utiliser l'ALU plus efficacement. Dès qu'un vecteur court est terminé, le processeur peut immédiatement démarrer le calcul d'un autre vecteur, le vecteur suivant, l'instruction suivante, sans laisser de cycles inutilisés. Avec un processeur SIMD non-vectoriel, on aurait du utiliser de la prédication, donc utiliser seulement une partie des ALU SIMD, ce qui aurait été un gâchis.
===Les accès mémoire===
Les accès mémoire des processeurs vectoriels sont sensiblement les mêmes que ceux des autres processeurs SIMD. Les instructions d'accès mémoire sont les mêmes, on retrouve les modes d'adressages précédents, dont les accès en ''scatter-gather'' et en ''stride''.
Sur certains processeurs vectoriels, les instructions vectorielles lisent et écrivent en mémoire RAM, sans passer par des registres : on parle de '''processeurs vectoriels mémoire-mémoire'''. Elles n'avaient pas d'instructions LOAD et STORE, les instructions arithmétiques géraient directement la lecture des opérandes en mémoire, et incrémentaient automatiquement les adresses à lire/écrire. Elles pouvaient gérer des vecteurs de taille arbitraire, la gestion d'un tableau se faisait parfois en une seule instruction ! Le problème est que l'absence de registres pour stocker les données lues réduisait grandement les performances.
[[File:Processeur vectoriel mémoire-mémoire.png|centre|vignette|upright=2|Processeur vectoriel mémoire-mémoire]]
Les processeurs vectoriels mémoire-mémoire étaient cependant minoritaires, la grosse majorité des processeurs vectoriels récents possèdent des registres vectoriels dédiés aux vecteurs. La plupart sont des architectures de type LOAD-STORE, mais pas toute. De fait, il n'y a pas de lien strict entre adressage des opérandes et processeurs vectoriel (ou SIMT). Seuls les processeurs SIMD de type SWAR ont une restriction là-dessus et sont systématiquement des architectures LOAD-STORE.
[[File:Processeur vectoriel à registres vectoriels.png|centre|vignette|upright=2|Processeur vectoriel à registres vectoriels.]]
Il faut noter que l'implémentation des accès mémoire est potentiellement différente de celle des autres processeurs SIMD. En principe, on peut utiliser une implémentation identique entre les deux. Mais le fait que les processeurs vectoriels sont pipelinés fait qu'il est préférable d'utiliser une implémentation différente, qui se base sur une RAM ou des caches pipelinés. Vu que l'ALU reçoit une/deux opérandes par cycle, l'idéal est que la mémoire ou le cache aillent au même rythme et soient donc pipelinés.Bien sur, il est possible d'utiliser une implémentation plus classique, avec des caches multiports, un chargement en plusieurs fois des vecteurs depuis le cache, des circuits pour réorganiser les accès mémoire, etc.
En théorie, les architectures vectorielles font peu usage de mémoires cache. Les transferts entre registres vectoriels et mémoire RAM sont directs, sans intermédiaire. La raison est que les applications qui tirent parti du SIMD ont une bonne localité spatiale, mais une mauvaise localité temporelle. Or, les caches de données profitent surtout de la localité temporelle pour donner de bonnes performances. De plus, rappelons que les processeurs vectoriels sont assez anciens et datent d'une époque où les transistors étaient précieux. Il valait mieux utiliser des transistors dans un gros cache d'instruction que pour un cache de donnée peu efficace.
==Les systèmes SIMD à plusieurs processeurs : le SIMT==
Enfin, terminons avec le dernier type de SIMD, qui date du tout début de l'informatique : celui où on combine plusieurs processeurs non-SIMD. L'idée est de synchroniser plusieurs processeurs de manière à ce qu'ils exécutent la même instruction en même temps, chacun sur des données différentes. On parle d''''exécution ''lockstep''''' des instructions. Toute la difficulté est de garantir la synchronisation des processeurs, garantir que tous les processeurs exécutent la même instruction. Ce qui pose problème quand des branchements sont impliqués, des processeurs pouvant prendre le branchement et pas les autres.
===Les ''array processors'' des temps anciens===
Les premières architectures matérielles conçues pour le parallélisme de données étaient de ce type. On peut par exemple citer l'exemple des Thinking machines CM-1 et CM-2, qui exécutaient tous la même instruction au même cycle d'horloge sur 64000 processeurs minimalistes. Un autre exemple est celui de l'ILLIAC IV. Historiquement, le terme utilisé pour désigner de telles architectures était '''array processors''', encore que ce terme fût réservé aux systèmes multiprocesseurs. Par la suite, le terme ''Single Instruction Multiple Threads'', ou SIMT, a été utilisé. Mais ce terme a ensuite été repris par le marketing de NVIDIA pour désigner une technique précise de SIMD avec prédication, utilisée sur ses cartes graphiques récentes. Le mauvais usage de la terminologie fait que le terme SIMT est devenu polysémique et quelque peu trompeur.
L'ILLIAC 4 était un ordinateur un peu particulier, presque unique en son genre. Son architecture était composé d'un processeur principal couplé à plusieurs processeurs secondaires. Il y avait en tout un processeur principal et 64 processeurs secondaires.
Le processeur principal chargeait les instructions depuis la mémoire, les décodaient, puis envoyait les instructions aux processeurs secondaires. Le processeur principal était le seul à avoir un ''program counter'', il était utilisé pour gérer les boucles, ainsi que pour configurer les instructions à prédication. Cependant, le processeur principal n'était pas un simple séquenceur : il pouvait exécuter des instructions, avait un accumulateur et des registres, etc. Il prenait en charge toute instruction non-SIMD, alors que les instructions SIMD étaient envoyées aux processeurs secondaires.
Les 64 processeurs secondaires avaient chacun : une unité de calcul, un ''local store'' de 2048 flottants de 64 bits, une unité mémoire. Ils n'ont pas de séquenceur ni de ''program counter'', ni de quoi charger une instruction. L'unité de calcul est appelée un ''processing element'' ou PE. Elle intègre un additionneur flottant, de quoi faire des décalages, et une unité de calcul logique. De plus, un PE intègre des registres accumulateurs et de quoi calculer des adresses : un registre d'indice, un registre d'adresse, un circuit de calcul d'adresse. L'unité mémoire connectait l'ALU entière PE et le ''local store'', et elle permettait aussi d'adresser des entrées-sorties. Pour gérer la prédication, les processeurs secondaires pouvaient être activés ou désactivés suivant le résultat des branchements/conditions.
Intuitivement, on peut faire le rapprochement avec un processeur SIMD. Le processeur principal regrouperait la partie non-SIMD d'un processeur SIMD, alors que les processeurs secondaires seraient l'unité de calcul SIMD. Cependant, il y a une différence très importante : il n'y a pas de banc de registre vectoriel. À la place, chaque processeur secondaire a son propre banc de registre, et ce sont des registres scalaires ! Une autre différence est que la mémoire RAM est découpée en plusieurs ''local store'' de 2048 adresses, chacun étant relié à une unité de calcul PE. N'oublions pas le système de calcul d'adresse et l'unité LOAD/STORE qui est fusionnée avec l'unité SIMD, chaque PE ayant ses circuits de calcul d'adresse, sa propre unité mémoire, etc.
===Les anciennes cartes graphiques hybrides SIMD-VLIW===
Une technique de SIMD multi-processeur a été utilisée autrefois sur certaines cartes graphiques AMD à architecture Terascale. La différence est que l'architecture en question était multicœur et non multi-processeurs. Elles contenaient plusieurs cœurs VLIW, qui fonctionnaient en ''lockstep''. Un groupe de 16 processeurs VLIWs exécutaient la même opération VLIW. Du moins, c'est ce que semblent dire les [https://www.techpowerup.com/gpu-specs/docs/amd-gcn1-architecture.pdf white papers de présentation de l'architecture GCN]. L'usage de cet hybride SIMD-VLIW était très adapté au traitement graphique.
Les cartes graphiques post-années 2000 sont capables d'exécuter des programmes appelés des '''''shaders''''', qui sont utilisés en rendu 3D, pour calculer certaines scènes 3D, et notamment pour calculer les ombres (d'où leur nom de shader, pour shade - ombre). Or, ces shaders sont justement exécutés par la carte graphique, sur les processeurs de shaders. Un processeur de ''shader'' applique un programme/''shader'' sur chaque ''pixel'' ou triangle d'une scène 3D (en fait des fragments et des vertices, mais faisons cette simplification). Chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres, ce qui rend le traitement 3D fortement parallèle. Aussi, l'usage du SIMD est assez naturel : chaque processeur exécute un ''shader'' sur un pixel/triangle, utiliser une architecture SIMD parait naturel. Reste à justifier l'usage de plusieurs CPU VLIW.
La raison tient dans la manière dont est traité un pixel par un shader. Un pixel est codé en utilisant quatre informations. Premièrement, une couleur codée avec trois nombres flottants, qui représentent une couleur au format RGB avec un mélange de rouge, vert et bleu, chaque couleur rouge/bleu/vert étant codée avec un flottant de 32 bits. La quatrième information est un nombre qui code la transparence du pixel. Généralement, le shader traite différemment la transparence de la couleur RGB, dans le sens où les instructions utilisées ne sont pas les mêmes. Au lieu de traiter couleur RGB et transparence séparément, elles sont traitées en même temps en parallèle, ce qui demande de pouvoir émettre deux instructions : une pour le calcul de la transparence, une autre pour la couleur RGB. Fait amusant, l'instruction utilisée pour gérer la composante RGB est une instruction vectorielle qui travaille sur des vecteurs de 3 couleurs. Les architectures VLIW sont parfaites pour cela.
Les cartes graphiques de l'époque de DirectX 9 utilisaient cette méthode qui mélangeait SIMD et VLIW. Elles disposaient de plusieurs cœurs VLIW qui exécutaient le même shader en exécution ''lockstep''. Chaque cœur traitait au moins un pixel à la fois, ou un triangle à la fois. Cependant, nous devons préciser que les cœurs VLIW partagent la même unité de contrôle, ce sont en réalité des chemins de données plus qu'autre chose. L'architecture était aussi combinée avec du ''Fine Grained Multithreading'' afin de pouvoir gérer les accès mémoire, de telles architectures ne pouvant pas se permettre d'utiliser l'exécution dans le désordre. De telles architectures multicœurs à exécution ''lockstep'' permettent quelques optimisations, comme le fait de partager le cache d'instruction entre les cœurs.
==Résumé des différents types de processeurs SIMD==
Dans ce chapitre, nous avons appris qu'il existe plusieurs catégories de processeurs SIMD. Les différences entre ces catégories tiennent à la fois dans le jeu d'instruction, mais aussi dans la microarchitecture des processeurs en question.
Au niveau du jeu d'instruction, il y a trois paliers, qui sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! Support de la prédication
! Taille des vecteurs
! Instructions horizontales
! Architecture LOAD-STORE
! Pointeur de pile
|-
! Processeurs SIMD purs
| Non
| rowspan="3" | Fixe
| rowspan="3" | Limitées à des échanges de données à l'intérieur d'un vecteur, pas d'instructions de réduction
| Oui
| rowspan="2" | Unique
|-
! Processeurs SIMD avec prédication
| Oui, par définition
| rowspan="3" | Dépend du processeur
|-
! Processeurs SIMT
| rowspan="2" | Oui, sauf exceptions
| Un pointeur de pile possible par élément du vecteur
|-
! Processeurs vectoriels
| Variable
| Toutes supportées, y compris les instructions arithmétiques horizontales
| Unique
|}
Au niveau de la microarchitecture, il y a trois paliers, qui sont résumés dans le tableau ci-dessous.
{|class="wikitable"
|-
!
! Unité de calcul
! Banc de registre
! Adressage des opérandes
|-
! Processeurs SIMD
| rowspan="2" | Plusieurs ALU simples non-pipelinée, plusieurs additionneurs/multiplieurs/autres
| Banc de registres vectoriels
| Architecture LOAD-STORE
|-
! Processeurs SIMT
| Plusieurs bancs de registres scalaires (non-vectoriels, registres normaux)
| rowspan="2" | Dépend du processeur
|-
! Processeurs vectoriels
| ALU simple unique, pipelinée
| Banc de registres vectoriels
|}
Les processeurs SIMD purs ont des performances différentes des processeurs vectoriels, vu que les premiers font leurs calculs en parallèle, alors que les CPU vectoriels utilisent un pipeline. Mais dans les grandes lignes, les performances sont similaires. Les différences sont grandement atténuées par l'usage du ''vector chaining'', de la gestion des vecteurs de taille variables, et des autres optimisations spécifiques aux processeurs vectoriels. L'avantage est cependant du côté des processeurs SIMD pur.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Architectures multithreadées et Hyperthreading
| prevText=Architectures multithreadées et Hyperthreading
| next=La cohérence des caches
| nextText=La cohérence des caches
}}
{{autocat}}
</noinclude>
oneimkpoh4x3n61lnnotxcigzq5c6ep
Les cartes graphiques/Les processeurs de shaders
0
69558
763321
758128
2026-04-09T15:02:38Z
Mewtow
31375
/* Les processeurs de shaders anciens : des processeurs VLIW */ Déplacement dans un chapitre séparé
763321
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Un processeur de ''shader'' reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.
Les '''registres d'entrée''' réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des ''shaders'' unifiés de DIrect X 10, les registres de sortie étaient différents entre les ''vertex shaders'' et les ''pixel shaders''. Les ''pixel shaders'' n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les ''vertex shaders'' avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
Les processeurs de shaders peuvent effectuer le même calcul sur plusieurs vertices ou plusieurs pixels à la fois. On dit que ce sont des processeurs parallèles, à savoir qu'ils peuvent faire plusieurs calculs en parallèle dans des unités de calcul séparées. Suivant la carte graphique, on peut les classer en deux types, suivant la manière dont ils exécutent des instructions en parallèle : les processeurs SIMD et les processeurs VLIW. Dans cette section, nous allons voir les processeurs SIMD.
Avant d'expliquer à quoi correspondent ces deux termes, sachez juste que l'usage de processeurs VLIW dans les cartes graphiques n'est plus très courant de nos jours. Il a existé des cartes graphiques assez anciennes qui utilisaient des processeurs de type VLIW, mais ce n'est plus en odeur de sainteté de nos jours. De nos jours, les processeurs de shaders sont tous des processeurs SIMD ou des dérivés (la technique dite du SIMT est une sorte de SIMD amélioré). Cependant, il arrive que même en étant des processeurs SIMD, certaines de leurs instructions soient inspirées des instructions VLIW.
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||miscellaneous
|}
L'instruction la plus intéressante est clairement la dernière : elle applique l'algorithme d'illumination de Phong sur un sommet. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais l'algo de Phong est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
dqfvkhdvdc0375a4vq8on160o8760ou
763329
763321
2026-04-09T15:07:24Z
Mewtow
31375
/* Les processeurs de shaders modernes : les processeurs SIMD */
763329
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Un processeur de ''shader'' reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.
Les '''registres d'entrée''' réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des ''shaders'' unifiés de DIrect X 10, les registres de sortie étaient différents entre les ''vertex shaders'' et les ''pixel shaders''. Les ''pixel shaders'' n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les ''vertex shaders'' avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quells sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
Les processeurs de shaders peuvent effectuer le même calcul sur plusieurs vertices ou plusieurs pixels à la fois. On dit que ce sont des processeurs parallèles, à savoir qu'ils peuvent faire plusieurs calculs en parallèle dans des unités de calcul séparées. Suivant la carte graphique, on peut les classer en deux types, suivant la manière dont ils exécutent des instructions en parallèle : les processeurs SIMD et les processeurs VLIW. Dans cette section, nous allons voir les processeurs SIMD.
Avant d'expliquer à quoi correspondent ces deux termes, sachez juste que l'usage de processeurs VLIW dans les cartes graphiques n'est plus très courant de nos jours. Il a existé des cartes graphiques assez anciennes qui utilisaient des processeurs de type VLIW, mais ce n'est plus en odeur de sainteté de nos jours. De nos jours, les processeurs de shaders sont tous des processeurs SIMD ou des dérivés (la technique dite du SIMT est une sorte de SIMD amélioré). Cependant, il arrive que même en étant des processeurs SIMD, certaines de leurs instructions soient inspirées des instructions VLIW.
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||miscellaneous
|}
L'instruction la plus intéressante est clairement la dernière : elle applique l'algorithme d'illumination de Phong sur un sommet. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais l'algo de Phong est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
ipsgz7qb4ibdnl6wy3x2h6yi7e5t4nt
763330
763329
2026-04-09T15:08:31Z
Mewtow
31375
/* Les processeurs de shaders modernes : les processeurs SIMD */
763330
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Un processeur de ''shader'' reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.
Les '''registres d'entrée''' réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des ''shaders'' unifiés de DIrect X 10, les registres de sortie étaient différents entre les ''vertex shaders'' et les ''pixel shaders''. Les ''pixel shaders'' n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les ''vertex shaders'' avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||miscellaneous
|}
L'instruction la plus intéressante est clairement la dernière : elle applique l'algorithme d'illumination de Phong sur un sommet. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais l'algo de Phong est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
jylvonws90v9nja0wy42l970x6opmxi
763380
763330
2026-04-09T21:04:10Z
Mewtow
31375
/* Un exemple : le jeu d’instruction du GPU de la Geforce 3 */
763380
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Un processeur de ''shader'' reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.
Les '''registres d'entrée''' réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des ''shaders'' unifiés de DIrect X 10, les registres de sortie étaient différents entre les ''vertex shaders'' et les ''pixel shaders''. Les ''pixel shaders'' n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les ''vertex shaders'' avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle applique l'algorithme d'illumination de Phong sur un sommet. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais l'algo de Phong est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
fmnxsoif45669nrxgdevy8671b8sicc
763381
763380
2026-04-09T21:05:29Z
Mewtow
31375
/* Un exemple : le jeu d’instruction du GPU de la Geforce 3 */
763381
wikitext
text/x-wiki
Les '''''shaders''''' sont des programmes informatiques exécutés par la carte graphique, et plus précisément par des processeurs de ''shaders''. Un point très important à comprendre est que chaque triangle ou pixel d'une scène 3D peut être traité indépendamment des autres. Le tout se résume comme suit :
: '''L’exécution d'un shader génère un grand nombre d'instances de ce shader, chacune traitant un paquet de pixels/sommets différent.'''
En conséquence, il est possible de traiter chaque instance d'un ''shader'' en parallèle des autres, en même temps, au lieu de traiter les instances l'une après l'autre.
La conséquence est que les cartes graphiques sont des architectures massivement parallèles, à savoir qu'elles sont capables d'effectuer un grand nombre de calculs indépendants en même temps. De plus, le parallélisme utilisé est du parallélisme de données, à savoir qu'on exécute le même programme sur des données différentes, chaque donnée étant traitée en parallèle des autres. Les cartes graphiques récentes incorporent toutes les techniques de parallélisme de donnée au niveau matériel, et nous allons toutes les détailler dans ce chapitre. S'il fallait résumer, elles ont plusieurs processeurs/cœurs, chaque cœur est capable d’exécuter des instructions SIMD (ils ne font que cela, à vrai dire), les cœurs sont fortement multithreadés, et j'en passe.
[[File:CPU and GPU.png|vignette|Comparaison du nombre de processeurs et de cœurs entre CPU et GPU.]]
Le premier point est qu'une carte graphique contient de nombreux processeurs, qui eux-mêmes contiennent plusieurs unités de calcul. Savoir combien de cœurs contient une carte graphique est cependant très compliqué, car la terminologie utilisée par les fabricants de carte graphique est particulièrement confuse. Il n'est pas rare que ceux-ci appellent cœurs ou processeurs, ce qui correspond en réalité à une unité de calcul d'un processeur normal, sans doute histoire de gonfler les chiffres. Et on peut généraliser à la majorité de la terminologie utilisée par les fabricants, que ce soit pour les termes ''warps processor'', ou autre, qui ne sont pas aisés à interpréter.
L'architecture d'une carte graphique récente est illustrée ci-dessous. Rien de bien déroutant pour qui a déjà étudié les architectures à parallélisme de données, mais quelques rappels ou explications ne peuvent pas faire de mal. Le premier point est la présence d'un grand nombre de processeurs/cœurs, les rectangles en bleu/rouge. Chacun d'entre eux contient un grand nombre de circuits de calculs, avec des circuits de calcul simples mais nombreux en rouge, et une unité pour les calculs complexes (trigonométriques, racines carrées, autres) en rouge. Le tout est relié à une hiérarchie mémoire indiquée en vert, comprenant des mémoires locales en complément de la mémoire vidéo principale. Le tout est alimenté par une unité de répartition, le '''''Thread Execution Control Unit''''' en jaune, qui répartit les différentes instances du ''shader'' sur les différents processeurs. Elle est aussi appelée le '''processeur de commandes''', comme nous le verrons dans quelques chapitres. Nous utiliserons le terme processeur de commande dans ce qui suit.
[[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts. Chacun d'entre eux contient plusieurs unités de calcul généralistes, appelées processeurs de threads, qui s'occupent de calculs simples (en bleu). D'autres calculs plus complexes sont pris en charge par une unité de calcul spécialisée (en rouge). Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]]
Les portions bleu, jaune et verte du schéma précédent méritent chacune un chapitre séparé. La hiérarchie mémoire en vert fera l'objet d'un chapitre ultérieur. Quant au répartiteur en jaune, il sera détaillé en profondeur dans le prochain chapitre. Dans ce chapitre, nous allons voir comment fonctionnent les processeurs de ''shaders'', la partie bleue. Nous allons voir que ceux-ci ne sont pas très différents des processeurs que l'on trouve dans les ordinateurs normaux, du moins dans les grandes lignes. Ce sont des processeurs séquentiels, qui exécutent des instructions les unes après les autres. Ils ont des instructions machines, des modes d'adressage, un assembleur, des registres et tout ce qui fait qu'un processeur est un processeur. Néanmoins, il y a une différence de taille : ce sont des processeurs adaptés pour effectuer un grand nombre de calculs en parallèle.
==Les registres des processeurs de shaders==
Un processeur de shaders contient beaucoup de registres, sans quoi il ne pourrait pas faire son travail efficacement. Les plus intuitifs sont les '''registres généraux''', aussi appelés registres temporaires, qui servent à mémoriser des résultats temporaires. Les registres temporaires sont les registres du processeur proprement dit, ceux qu'il peut manipuler à loisir. Tout processeur digne de ce nom en possède. Mais un processeur de ''shader'' dispose aussi de registres spécialisés, qu'on ne trouve que sur les processeurs de ''shaders'', qui servent à l'interfacer avec le reste du pipeline graphique.
[[File:Architecture carte graphique vertex avec texture.PNG|centre|vignette|upright=2|Architecture carte graphique vertex avec texture]]
===Les registres d'interface avec le pipeline graphique===
Un processeur de ''shader'' reçoit des données provenant de l'unité de rastérisation, et envoie son résultat final aux ROPs. Il y a donc des registres d'entrée et de sortie spécialisés pour faire l'interface entre les deux. Ils servent d'interface avec le reste du pipeline graphique, notamment le rastérizeur et les ROPs, mais aussi avec les unités de texture.
Les '''registres d'entrée''' réceptionnent les vertices/pixels provenant de l'unité de rastérisation. Les registres d'entrée sont en lecture seule, du point de vue du processeur de shader, seule l'unité de rastérisation peut écrire dedans. Ils sont initialisés avant l'exécution du ''shader''.
Les '''registres de sortie''' sont là où le processeur stocke les résultats à envoyer aux ROP. Les registres de sorties sont en écriture seule. Avant l'apparition des ''shaders'' unifiés de DIrect X 10, les registres de sortie étaient différents entre les ''vertex shaders'' et les ''pixel shaders''. Les ''pixel shaders'' n'avaient que deux registres de sorties : un pour la couleur à envoyer aux ROP, un autre pour la profondeur du pixel. Les ''vertex shaders'' avaient eu beaucoup plus de registres de sorties, vu que l'unité de rastérisation avait besoin de beaucoup d'information. Il y avait au minimum un registre pour la position du sommet dans l'espace (trois coordonnées), un autre pour la couleur/luminosité du sommet, un autre pour la couleur du brouillard, un autre pour les coordonnées de texture.
{|class="wikitable"
|+ Registres de sortie des ''pixel/vertex shaders''
|-
! Vertex shader
! Pixel shader
|-
| Couleur du pixel
| Couleur du sommet
|-
| Profondeur du pixel
| Position du sommet
|-
| rowspan="2" |
| Coordonnées de texture du sommet
|-
| Couleur de brouillard.
|}
Il y a aussi des '''registres de texture''' , qui servent d'interface avec la mémoire pour la gestion des textures. Ils mémorisent les texels lus par l'unité de texture. L'unité de texture lit un texel, plusieurs avec ''multitexturing'', et les place dans ces registres de texture. Les registres de texture sont parfois initialisés avant l'exécution du ''shader'', mais la plupart sont initialisé quand le ''shader'' termine une instruction de lecture de texture. Ils sont généralement en lecture seule, mais il y a des exceptions.
===Les registres spécialisés internes===
D'autres registres spécialisés ne font pas l'interface avec le reste du GPU. Ils servent à stocker des constantes ou des données importantes, qui n'ont pas vraiment leur place dans les registres généraux.
Les '''registres de constantes''' servent pour stocker des constantes utiles pour le ''shader''. Par exemple, pour les ''vertex shaders'', ils stockent les matrices servant aux différentes étapes de transformation ou d'éclairage. Ces constantes sont placées dans ces registres peu après le chargement du vertex shader dans la mémoire vidéo. Toutefois, le vertex shader peut écrire dans ces registres, au prix d'une perte de performance particulièrement violente.
Les ''pixel/vertex shaders'' 1.0 ne géraient que des constantes flottantes pour les ''vertex shaders'', entières pour les ''pixel shaders''. Mais les ''pixel/vertex shaders'' 2.0 et 3.0 avaient des registres de constantes séparés pour les nombres entiers, les nombres flottants, et même les nombres booléens. Les constantes entières et booléennes étaient utilisées pour gérer les boucles, guère plus. Aussi, il y en avait 16, comparé aux centaines de registres de constantes flottants. Mais avec les ''pixel/vertex shaders'' 4.0 et plus, les registres de constante ont été fusionnés et n'ont plus de type prédéterminé, le programmeur gère ces registres comme il l'entend.
L'adressage des registres de constante est quelque peu particulier. Il faut dire qu'il y en a plusieurs milliers sur les processeurs de ''shaders'' modernes, au point qu'il serait plus juste de parler de mémoire RAM des constantes. Les registres de constante sont en effet un ''local store'' un peu spécial, intégré directement dans le processeur. Et le processeur accède à ce ''local store'' en utilisant une mode d'adressage semblable à celui utilisé pour la mémoire, avec un mode d'adressage indirect. L'adresse à lire dans ce ''local store'' est dans un registre, séparé du reste, appelé le '''registre d'adresse de constante'''.
Depuis les ''pixel/vertex shaders'' 3.0, les ''shaders'' sont capables d'effectuer des boucles et d'autres structures de contrôle familières pour les programmeurs. Et deux registres ont été intégrés afin d'améliorer les performances des structures de contrôle. Le premier est un registre à prédicat, qui sera vu dans la section sur le SIMD avec prédication. Le second est un '''registre compteur de boucle''', qui mémorise l'indice d'une boucle. Il est initialisé à 0, et est incrémenté à chaque fois qu'une boucle s'exécute.
==Les processeurs de shaders modernes : les processeurs SIMD==
Maintenant, voyons quelles sont les instructions supportées par les processeurs de shaders modernes. Et si je dis moderne, c'est car nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9 et antérieur. La raison est que le jeu d'instruction des shaders a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Le jeu d'instruction des GPU NVIDIA n'est pas encore connu à l'heure où j'écris ces lignes, la documentation du constructeur n'est pas disponible. Quelques chercheurs ont tenté de faire de la rétro-ingénierie du code de divers shaders pour retrouver le jeu d'instruction des divers GPU NVIDIA, ce qui fait qu'on a cependant une idée de ce dernier. Mais rien d'officiel. Par contre, AMD fournit librement cette documentation sur le net. Ce qui fait qu'on peut trouver des documents de ce genre :
* [https://developer.amd.com/wordpress/media/2012/12/AMD_Southern_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 1 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/07/AMD_Sea_Islands_Instruction_Set_Architecture.pdf Graphics Core Next 2 instruction set] ;
* [https://developer.amd.com/wordpress/media/2013/12/AMD_GCN3_Instruction_Set_Architecture_rev1.1.pdf Graphics Core Next 3 and 4 instruction sets] ;
* [https://developer.amd.com/wp-content/resources/Vega_Shader_ISA_28July2017.pdf Graphics Core Next 5 instruction set] ;
* [https://developer.amd.com/wp-content/resources/Vega_7nm_Shader_ISA.pdf "Vega" 7nm instruction set architecture] (also referred to as Graphics Core Next 5.1) ;
* [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna3-shader-instruction-set-architecture-feb-2023_0.pdf Jeu d'instruction des GPU de type RDNA3 d'AMD].
===Les instructions SIMD===
Les '''instructions SIMD''' manipulent plusieurs nombres en même temps. Elles manipulent plus précisément des '''vecteurs''', des ensembles de plusieurs nombres entiers ou nombres flottants placés les uns à côté des autres, le tout ayant une taille fixe, qui sont stockés dans des registres spécialisés. En général, tous les vecteurs ont une taille fixe, peu importe leur contenu. Cela implique que suivant la taille des données à manipuler, on pourra en placer plus ou moins dans un vecteur. Par exemple, un vecteur de 128 bits pourra contenir 4 entiers de 32 bits, 4 flottants 32 bits, ou 8 entiers de 16 bits.
[[File:Vector register.png|centre|vignette|upright=2|Contenu d'un vecteur en fonction du type de données utilisé.]]
Les vecteurs sont stockés dans des '''registres vectoriels''', aussi appelés '''registres SIMD'''. Un registre vectoriel peut contenir un vecteur complet, pas plus. En conséquence, ils ont une taille assez importante : ils font généralement 128, 256, voire 512 bits, comparé aux 32/64 bits des registres des CPU. Les cartes graphiques modernes contiennent un très grand nombre de registres SIMD.
{|
|+ Comparaison entre un processeur sans registres vectoriels, et avec registres vectoriels.
|[[File:Non-SIMD cpu diagram1.svg|vignette|upright=1.5|CPU Non-SIMD]]
|[[File:SIMD cpu diagram1.svg|vignette|upright=1.5|CPU SIMD]]
|}
Une instruction SIMD traite chaque donnée du vecteur indépendamment des autres. Par exemple, une instruction d'addition vectorielle va additionner ensemble les données qui sont à la même place dans deux vecteurs, et placer le résultat dans un autre vecteur, à la même place.
[[File:Instructions SIMD.png|centre|vignette|upright=2.0|Instructions SIMD]]
Sur les cartes graphiques modernes, les vecteurs sont généralement des vecteurs qui regroupent plusieurs nombres flottants. De plus, les flottants en question sont des flottants dits simple précision, codés sur 32 bits. Mais il y a quelques exceptions, comme [https://www.realworldtech.com/apple-custom-gpu/ certains GPU d'Apple, qui ne gèrent majoritairement que des flottants codés sur 16 bits], avec des fonctionnalités pour la simple précision. Les anciennes cartes graphiques ne géraient pas du tout de vecteurs contenant des nombres entiers.
===Les instruction scalaires entières, typiques des CPU===
Un processeur SIMD gère donc des instructions SIMD, et les anciennes cartes graphiques ne disposaient que d'instructions de ce type. Mais depuis au moins une décennie, les processeurs de shaders gèrent des instructions normales, non-SIMD. De telles instructions sont appelées des '''instruction scalaires'''. En clair, il s'agit des instructions qu'on retrouve normalement tous les processeurs principaux (les CPU).
Il s'agit généralement d''''instructions entières''', agissent sur des registres entiers non-SIMD. Elles ne traitent pas de vecteur, mais de simples nombres entiers indépendants, sans regroupement d'aucune sorte. Typiquement, il s'agit d'opérations d'addition, de soustraction, des opérations logiques, des comparaisons, guère plus. On trouve aussi des opérations un peu originales, comme des calculs de valeur absolue, du minimum/maximum de deux opérandes, des opérations à prédicat comme une instruction CMOV, etc. Les cartes graphiques supportent rarement la multiplication, mais les plus récentes supportent des multiplications sur des opérandes de 16/32 bits. Par contre, aucune ne gère de division entière.
Les GPU modernes gèrent aussi des instructions de test et de branchement, là encore sur des nombres entiers. Les instructions de test et branchement sont généralement considérées comme à part des instructions de calcul, mais ce sont des opérations scalaires. Les comparaisons se font entre deux entiers scalaires, pas entre deux vecteurs. Retenez bien ce détail, car il sera très important pour la suite.
Les GPU modernes gèrent aussi des '''instructions flottantes scalaires''', à savoir que des instructions qui ont pour opérandes des nombres flottants isolés, qui ne sont pas dans un vecteur. Les processeurs principaux (CPU) d'un ordinateur sont capables de faire beaucoup de calculs arithmétiques simples sur des nombres flottants, comme des additions, des multiplications, des opérations bit-à-bit, éventuellement des divisions, etc. Il en est de même sur les GPUS. Mais ces derniers gèrent aussi de nombreuses instructions flottantes que les CPU n'incorporent presque pas.
Il est rare que les CPU soient capables de faire des opérations flottantes complexes, comme des calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse, etc. De tels calculs sont rares dans les programmes exécutables, alors que les calculs arithmétiques simples y sont légion. Mais le rendu 3D demande pas mal de calculs trigonométriques, de produits scalaires ou d'autres opérations. Par exemple, dans les chapitres précédents, nous avions abordé les calculs d'éclairage et avions vu qu'ils font beaucoup de calculs vectoriels avec des vecteurs comme la normale d'un sommet. Et ces calculs demandent de calculer des produits scalaires et vectoriels, qui eux-mêmes demandent des calculs trigonométriques comme le cosinus ou le sinus.
Aussi, les processeurs de ''shaders'' disposent souvent d'instructions flottantes spécialisées dans les calculs complexes : exponentielle/logarithme, racine carrée, racine carrée inverse, autres. Nous appellerons ces instructions des '''instructions transcendantales''', car elles effectuent des calculs de ce type.
Il faut noter que le processeur incorpore des registres dédiés aux scalaires, séparés des registres SIMD. Par séparés, on veut dire que ce sont des registres différents, adressés différemment, mais qu'ils sont aussi physiquement séparés dans le processeur, ils sont des bancs de registres différents.
===Les instructions en ''co-issue''===
Beaucoup de cartes graphiques récentes comme anciennes incorporent des '''instructions de ''co-issue''''' qui ne se trouvent que sur les cartes graphiques et n'ont aucun équivalent sur les CPUs. Les instructions de ''co-issue'' regroupent plusieurs opérations par instruction. Par exemple, elles peuvent combiner une opération vectorielle avec une opération scalaire. Ou encore, elles peuvent regrouper une opération scalaire, une opération vectorielle et un branchement. Il s'agit d'instructions qui ressemblent grandement à ce qu'on trouve sur les processeurs VLIW.
Un point important est que les cartes graphiques modernes disposent d'instructions à ''co-issue'' en plus des instructions normales. Les instructions à ''co-issue'' sont complémentaire des instructions normales, elles ne les remplacent pas. Les deux peuvent s'utiliser en même temps, dans un même shader. Il a cependant existé des cartes graphiques assez anciennes sur lesquelles toutes les instructions étaient des instructions à ''co-issue'' : certains processeurs de shaders VLIW anciens sont de ce type.
Il y a de nombreuses contraintes quant au regroupement des deux opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre. L'exemple type de ''co-issue'' est la ''co-issue'' entre opérations scalaires et vectorielles : il n'est pas possible de regrouper deux instructions scalaires ou deux instructions vectorielles. La seule possibilité est de regrouper une opération scalaire et une opération vectorielle. La raison à cela est qu'opérations scalaires et vectorielles sont calculées dans des circuits séparés : le processeur incorpore une unité de calcul scalaire et une unité de calcul SIMD, et peut utiliser les deux en parallèle, en même temps. Mais nous verrons cela dans quelques chapitres.
Pour simplifier, cette technique permettait d’exécuter deux opérations arithmétiques en même temps, en parallèle : une opération vectorielle appliquée aux couleurs R, G, et B, et une opération scalaire appliquée à la couleur de transparence. Si cela semble intéressant sur le papier, cela complexifie fortement le processeur de shader, ainsi que la traduction à la volée des shaders en instructions machine.
===Un exemple : le jeu d’instruction du GPU de la Geforce 3===
La première carte graphique commerciale grand public à disposer d'une unité de vertex programmable est la Geforce 3. Celui-ci respectait le format de vertex shader 1.1. L'ensemble des informations à savoir sur cette unité est disponible dans l'article [https://cseweb.ucsd.edu/~ravir/6160-fall04/papers/p149-lindholm.pdf "A user programmable vertex engine"], disponible sur le net. . Le processeur de cette carte était capable de gérer un seul type de données : les nombres flottants de norme IEEE754. Toutes les informations concernant la coordonnée d'une vertice, voire ses différentes couleurs, doivent être encodées en utilisant ces flottants.
Les processeurs de vertices de la Geforce 3 disposent de registres registres SIMD qui font 128 bits, soit 4 flottants de 32 bits. Elle contient 16 registres d'entrée, 16 registres de sortie, 32 registres généraux. La mémoire des constantes contient 512 "registres".
Le processeur de la Geforce 3 est capable d’exécuter 17 instructions différentes, dont voici les principales :
{|class="wikitable"
|-
!OpCode!!Nom!!Description
|-
! colspan="3" | Opérations mémoire
|-
|MOV||Move||vector -> vector
|-
|ARL||Address register load||miscellaneous
|-
! colspan="3" | Opérations arithmétiques
|-
|ADD||Add||vector -> vector
|-
|MUL||Multiply||vector -> vector
|-
|MAD||Multiply and add||vector -> vector
|-
|MIN||Minimum||vector -> vector
|-
|MAX||Maximum||vector -> vector
|-
|SLT||Set on less than||vector -> vector
|-
|SGE||Set on greater or equal||vector -> vector
|-
|LOG||Log base 2||miscellaneous
|-
|EXP||Exp base 2||miscellaneous
|-
|RCP||Reciprocal||scalar-> replicated scalar
|-
|RSQ||Reciprocal square root||scalar-> replicated scalar
|-
! colspan="3" | Opérations trigonométriques
|-
|DP3||3 term dot product||vector-> replicated scalar
|-
|DP4||4 term dot product||vector-> replicated scalar
|-
|DST||Distance||vector -> vector
|-
! colspan="3" | Opérations d'éclairage géométrique
|-
|LIT||Phong lighting||Calcule l'éclairage de Gouraud
|}
L'instruction la plus intéressante est clairement la dernière : elle éclaire un sommet, en utilisant un éclairage de Phong. Les autres instructions permettent d'implémenter un autre algorithme si besoin, mais cette forme d'éclairage est déjà là à la base.
Les autres instructions sont surtout des instructions arithmétiques : multiplications, additions, exponentielles, logarithmes, racines carrées, etc. Pour les instructions d'accès à la mémoire, on trouve une instruction MOV qui déplace le contenu d'un registre dans un autre et une instruction de calcul d'adresse, mais aucune instruction d'accès à la mémoire sur le processeur de la Geforce 3. Plus tard, les unités de ''vertex shader'' ont acquis la possibilité de lire des données dans une texture.
On remarque que la division est absente. Il faut dire que la contrainte qui veut que toutes ces instructions s’exécutent en un cycle d'horloge pose quelques problèmes avec la division, qui est une opération plutôt lourde en hardware. À la place, on trouve l'instruction RCP, capable de calculer 1/x, avec x un flottant. Cela permet ainsi de simuler une division : pour obtenir Y/X, il suffit de calculer 1/X avec RCP, et de multiplier le résultat par Y.
==La prédication et le SIMT==
Les cartes graphiques récentes peuvent effectuer des branchements, mais ceux-ci sont tout sauf performants. Dès qu'un branchement survient, le processeur est obligé de traiter chaque élément du vecteur un par un, au lieu de tous les traiter en même temps en parallèle. Les performances s'en ressentent, ce qui fait que les branchements sont à éviter le plus possible. Pour améliorer la gestion des conditions, les cartes graphiques modernes incorporent des instructions spécialisées qui permettent de remplacer des codes remplis de branchements par des codes plus simples, compatibles avec l'organisation des données en vecteurs.
Si on met de côté le support de certaines instructions courantes, comme la valeur absolue, ou le calcul du minimum/maximum, la technique la plus importante est la technique dite de '''prédication'''. L'idée est que quand une instruction effectue un calcul sur un ou deux vecteurs, certains éléments du vecteur sont ignorés. Les éléments à ignorer sont choisis suivant le résultat d'une instruction de comparaison, qui effectue un test : les éléments pour lesquels ce test est respecté sont pris en compte, ceux qui ne passent pas le test sont ignorés.
Pour donner un exemple d'utilisation, imaginons que l'on ait un vecteur dans lequel on veut remplacer toutes les valeurs négatives par des 0. Dans ce cas, on utilise :
* une instruction de comparaison, qui compare chaque élément du vecteur avec 0 et génère plusieurs bits de résultat ;
* suivi d'une instruction à prédicat qui met à zéro les éléments pour lesquels les bits de résultat précédents sont à 1.
Elle est implémentée grâce à un registre appelé le '''''Vector Mask Register'''''. Celui-ci permet de stocker des informations qui permettront de sélectionner certaines données et pas d'autres pour faire notre calcul. Il est mis à jour par des instructions de comparaison. le ''Vector Mask Register'' stocke un bit pour chaque flottant présent dans le vecteur à traiter, bit qui indique s'il faut appliquer l'instruction sur ce flottant. Si ce bit est à 1, notre instruction doit s’exécuter sur la donnée associée à ce bit. Sinon, notre instruction ne doit pas la modifier. On peut ainsi traiter seulement une partie des registres stockant des vecteurs SIMD.
[[File:Vector mask register.png|centre|vignette|upright=2.0|''Vector mask register'']]
===La prédication avec une pile SIMT===
Au niveau du jeu d’instruction, les architectures SIMT implémentent de la prédication, sous une forme améliorée. Les processeurs SIMT actuels sont surtout utilisées sur les processeurs intégrés aux cartes graphiques. Et ces derniers gèrent très mal les branchements, et encore : beaucoup de cartes graphiques, même récentes, ne gèrent tout simplement pas les branchements. Elles doivent donc se débrouiller avec uniquement la prédication, là où les processeurs SIMD utilisent des branchements normaux en complément de la prédication. Insistons sur le fait que cet usage exclusif de la prédication n'est présent que sur une sous-partie des architectures SIMT, le seul exemple que l'auteur de ce wikilivre connait étant celui des cartes graphiques.
Les architectures SIMT sans branchements doivent donc trouver des solutions pour gérer les structures de contrôle imbriquées, à savoir une boucle placée à l'intérieur d'une autre boucle, un IF...ELSE dans un autre IF...ELSE, etc. Elles utilisent pour cela la prédication, combinée avec des mécanismes annexes. Le premier d'entre eux est l'usage de plusieurs registres de masques organisés d'une manière bien précise, l'autre est l'usage de compteurs d'activité. Voyons ces deux techniques.
La '''pile de masques''' remplace le ou les registres de masque. Sans elle, le processeur SIMD incorpore un registre de masque qui est adressé implicitement ou explicitement. Éventuellement, le processeur peut contenir plusieurs registres de masque séparés adressables via un nom de registre. Avec elle, le processeur SIMD incorpore plusieurs registres de masque organisé en pile. Le registre de masque est donc remplacé par une mémoire LIFO, une pile, dans laquelle plusieurs masques sont empilés.
Le tout forme une pile, similaire à la pile d'appel, sauf qu'elle est utilisée pour empiler des masques. Un masque est calculé et empilé à chaque entrée dans une structure de contrôle, puis dépilé une fois la structure de contrôle exécutée. L'empilement et le dépilement des masques est effectué par des instructions PUSH et POP, présentes dans le jeu d'instruction du processeur SIMD.
Le calcul des masques doit répondre à plusieurs impératifs.
* Premièrement, chaque masque se calcule en faisant un ET entre le masque précédent et le masque calculé par l'instruction de test. Cela permet de ne pas réveiller d’élément au beau milieu d'une structure imbriquée. Si in IF désactive certains éléments du vecteur, une condition imbriquée dans ce IF ne doit pas réveiller cet élément. Le fait de faire un ET entre les masques garantit cela.
* Deuxièmement, les masques doivent être empilés et dépilés correctement. Au moment de rentrer dans une structure de contrôle, on effectue une instruction de test associée à la structure de contrôle, qui calcule un masque, et on empile le masque calculé. Au moment de sortir de la structure de contrôle, on dépile le masque en question.
L'implémentation demande d'utiliser une mémoire LIFO pour stocker la pile de masques, et quelques circuits annexes. Il faut notamment un circuit relié à l'ALU qui récupère les conditions, les résultats des comparaisons, et qui effectue le ET pour combiner les masques.
Pour donner un exemple, prenons le code suivant, qui est volontairement simpliste et ne sert qu'à des fins d'explication :
<syntaxhighlight lang="c">
if ( condition 1 )
{
if ( condition 2 )
{
...
}
else
{
...
}
Autres instructions
}
Instructions après le IF...
</syntaxhighlight>
Imaginons que l'on traite des vecteurs de 8 éléments.
Pour le vecteur considéré, la première condition (a > 0) n'est respectée que par les 4 premiers éléments. L'instruction de condition calcule alors le masque correspondant : 1111 0000. Le masque est alors calculé, puis empilé au sommet de la pile.
La seconde instruction de test, qui teste la variable b, est maintenant valide pour les 4 bits du milieu du masque. Mais n'allez pas croire que le masque correspondant soit 0011 11100 : il faut tenir compte de la condition précédente, qui a éliminé les 4 derniers éléments. Pour cela, on fait un ET logique entre le masque précédent, et le masque calculé par la condition. Le masque au sommet de la pile est donc lu, combiné avec le masque calculé par l'instruction, ce qui donne le masque final. Le masque final est alors empilé au sommet de la pile.
On exécute alors l'instruction du IF, en tenant compte du masque qui est au sommet de la pile. Si le IF était plus compliqué, toutes les instructions suivantes tiendraient compte du masque. En fait, le masque est pris en compte tant qu'il n'est pas dépilé. Une fois que le IF est terminé, le masque est dépilé.
On passe alors au ELSE, et rebelotte. Le masque pour le ELSE est calculé en combinant le masque au sommet de la pile avec la condition du ELSE. Le masque au sommet de la pile est celui calculé à l'entrée du premier IF, pas le second qui a été dépilé. Les instructions du ELSE sont alors exécutées en tenant compte de ce masque. Une fois qu'elles sont toutes exécutées, le masque est dépilé.
Puis vient l'exécution des instructions après le ELSE. Elles utilisent le masque empilé au sommet de la pile, qui correspond à celui à l'entrée du IF.
Puis vient le moment d'exécuter les instructions après le IF : pas de masque, on exécute sur tout le vecteur.
===Les compteurs d'activité===
Une variante de la technique précédente remplace la pile de masques par des '''compteurs d'activité'''. La technique est similaire, si ce n'est qu'elle utilise moins de circuits. Avant , on avait une pile de masques de même taille, dont les bits sont à 0 ou 1 suivant que la condition est remplie. La pile de masque ressemble donc à ceci :
{|class="wikitable"
|-
! masque 1
| 1 || 1 || 1 || 1
|-
! masque 2
| 0 || 1 || 1 || 1
|-
! masque 3
| 0 || 1 || 1 || 1
|-
! masque 4
| 0 || 0 || 0 || 1
|-
! masque 1
| colspan="4" | vide
|}
Une manière équivalente de représenter cette pile de masque est de compter combien de bits sont à 0 dans chaque colonne. Attention : j'ai bien dit à 0 ! On obtient alors :
{|class="wikitable"
|-
! masque 1
| 3 || 1 || 1 || 0
|}
Et c'est le principe caché derrière la technique des compteurs d'activité. Chaque élément dans un vecteur, chaque place, se voit attribuer un compteur. Un compteur non-nul indique qu'il ne faut pas prendre en compte l’élément. Ce n'est qu'une fois que le compteur est nul que l'on effectue des opérations sur l’élément associé du vecteur.
À chaque fois qu'on entre dans une structure de contrôle, on teste une condition sur chaque élément. Si la condition est respectée pour un élément, alors le compteur ne change pas. Mais si la condition n'est pas respectée, alors on incrémente le compteur associé. En sortant de la structure de contrôle, on décrémente le compteur associé. Notons que les compteurs qui n'ont pas été incrémentés en entrant dans la structure de contrôle ne sont pas décrémentés en sortant. En clair, là où on empilait/dépilait un masque, on se contente d'incrémenter/décrémenter un compteur.
Utiliser un compteur en lieu et place d'une colonne entière dans la pile de masque utilise moins de bits. Et c'est sans doute pour cette raison que certaines cartes graphiques, comme les cartes graphiques intégrées d'Intel depuis 2004, utilisent cette technique.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes accélératrices 3D
| prevText=Les cartes accélératrices 3D
| next=La microarchitecture des processeurs de shaders
| nextText=La microarchitecture des processeurs de shaders
}}
{{autocat}}
66t4uvx9grx12krry9j2lefylhicqjk
Les cartes graphiques/Sommaire
0
70681
763319
758125
2026-04-09T14:34:24Z
Mewtow
31375
/* Les processeurs de shader */
763319
wikitext
text/x-wiki
* [[Les cartes graphiques/Les cartes d'affichage|Introduction : les cartes d’affichage]]
===Les cartes d'affichage et d'accélération 2D===
* [[Les cartes graphiques/Le Video Display Controler|Le Video Display Controler]]
* [[Les cartes graphiques/Les systèmes à framebuffer|Les systèmes à framebuffer]]
* [[Les cartes graphiques/Les cartes accélératrices 2D|Les cartes accélératrices 2D]]
* [[Les cartes graphiques/Le mode texte et le rendu en tiles|Le mode texte et le rendu en tiles]]
* [[Les cartes graphiques/Les accélérateurs de scanline|Les accélérateurs de scanline]]
* [[Les cartes graphiques/Les Video Display Controler atypiques|Les Video Display Controler atypiques]]
* [[Les cartes graphiques/Les cartes d'affichage des anciens PC|Les cartes d'affichage des anciens PC]]
===Les cartes accélératrices 3D===
* [[Les cartes graphiques/Le rendu d'une scène 3D : concepts de base|Le rendu d'une scène 3D : concepts de base]]
* [[Les cartes graphiques/Les cartes graphiques : architecture de base|Les cartes graphiques : architecture de base]]
* [[Les cartes graphiques/Les cartes accélératrices 3D|Les cartes accélératrices 3D]]
===Les processeurs de ''shader''===
* [[Les cartes graphiques/Les processeurs de shaders|Les processeurs de shaders]]
* [[Les cartes graphiques/La microarchitecture des processeurs de shaders|La microarchitecture des processeurs de shaders]]
* [[Les cartes graphiques/Les processeurs de shader VLIW et DirectX 9|Les processeurs de shader VLIW et DirectX 9]]
* [[Les cartes graphiques/Les caches d'un processeur de shader|Les caches d'un processeur de shader]]
===La mémoire vidéo (VRAM)===
* [[Les cartes graphiques/La mémoire unifiée et la mémoire vidéo dédiée|La mémoire unifiée et la mémoire vidéo dédiée]]
===Le processeur de commande===
* [[Les cartes graphiques/Le rendu d'une scène 3D : l'API graphique|Le rendu d'une scène 3D : l'API graphique]]
* [[Les cartes graphiques/Le processeur de commandes|Le processeur de commandes]]
* [[Les cartes graphiques/La répartition du travail sur les unités de shaders|La répartition du travail sur les unités de shaders]]
===Le pipeline fixe, non-programmable===
* [[Les cartes graphiques/Le pipeline géométrique d'avant DirectX 10|Le pipeline géométrique d'avant DirectX 10]]
* [[Les cartes graphiques/Le pipeline géométrique après DirectX 10|Le pipeline géométrique après DirectX 10]]
* [[Les cartes graphiques/Le rasterizeur|Le rasterizeur]]
* [[Les cartes graphiques/Les unités de texture|Les unités de texture]]
* [[Les cartes graphiques/Les Render Output Target|Les Render Output Target]]
===Annexe===
* [[Les cartes graphiques/Le support matériel du lancer de rayons|Le support matériel du lancer de rayons]]
* [[Les cartes graphiques/L'antialiasing|L'antialiasing]]
* [[Les cartes graphiques/Le multi-GPU|Le multi-GPU]]
==En savoir plus==
* [https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-graphics-pipeline-2011-index/ A trip through the Graphics Pipeline 2011: Index]
{{autocat}}
trx9pieo25x9mgfh4gbzejtlc2bogqw
Les cartes graphiques/Les caches d'un processeur de shader
0
74269
763325
763123
2026-04-09T15:03:34Z
Mewtow
31375
/* La cohérence des caches entre processeurs de shaders */
763325
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir comment est organisée la mémoire d'un GPU, ou plutôt devrait-on dire les mémoires d'un GPU. Eh oui : un GPU contient beaucoup de mémoires différentes. Un GPU contient évidemment une mémoire vidéo de grande taille, séparée des processeurs de shader, mais pas que. Les processeurs de shaders intègrent aussi des mémoires plus petites, appelées des mémoires caches. Les processeurs intégrent tous des caches et les processeurs de shaders ne font pas exception. Cependant, les caches d'un GPU sont quelque peu particuliers et sont organisés différemment. La hiérarchie mémoire des GPUs est assez particulière, et nous allons voir en quoi dans ce qui suit.
==Les caches spécialisés d'un GPU==
Un point important est que les GPU sont dédiés au rendu 3D, et cette spécialisation se voit dans leurs mémoires caches. Les premières cartes graphiques avaient des caches spécialisés, avec des caches pour les textures, des caches de sommets, des caches pour le tampon de profondeur, etc. Ils n'avaient pas de caches généralistes, qui servent à stocker n'importe quel type de données. Les caches spécialisés étaient intégrés aux circuits fixes. Par exemple, le cache pour les textures est placé dans l'unité de texture, le cache de sommet dans l'''input assembler'', le cache du ''z-buffer'' dans les ROPs.
===Les caches de sommets===
Avant Direct X 10, les cartes graphiques avaient des caches dédiés à la géométrie, deux précisémment. Ils étaient appelés des caches de sommets, le terme étant utilisé pour les deux caches. Le premier cache mémorise des sommets qui ont été transformés/éclairés, alors que le second mémorise des sommets pas encore éclairés. Le premier cache est appelé le ''Post-transform cache'' et se situe en sortie des unités de ''vertex shader'' ou de l'unité de T&L. Le second cache s'appelle le ''Pre-transform cache'' fait partie de l'''input assembler''.
[[File:Vertex cache.png|centre|vignette|upright=2.5|''Pre-transform'' et ''Post-transform cache''.]]
Nous détaillerons le fonctionnement de ces caches dans le chapitre sur le pipeline géométrique, nous ne pouvons pas en dire plus pour le moment. De plus, les deux caches ont disparus sur certains GPU modernes. Le ''Pre Transform Cache'' a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. Le ''Post-transform cache'' a lieu été remplacé, en raison de la manière dont les processeurs de shaders fonctionnent. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet.
===Le cache de textures===
Le '''cache de textures''', comme son nom l'indique, est un cache spécialisé dans les textures. Toutes les cartes graphiques modernes disposent de plusieurs unités de texture, qui disposent chacune de son ou ses propres caches de textures. Pas de cache partagé, ce serait peu utile et trop compliqué à implémenter.
De plus, les cartes graphiques modernes ont plusieurs caches de texture par unité de texture. Généralement, elles ont deux caches de textures : un petit cache rapide, et un gros cache lent. Les deux caches sont fortement différents. L'un est un gros cache, qui fait dans les 4 kibioctets, et l'autre est un petit cache, faisant souvent moins d'1 kibioctet. Mais le premier est plus lent que le second. Sur d'autres cartes graphiques récentes, on trouve plus de 2 caches de textures, organisés en une hiérarchie de caches de textures similaire à la hiérarchie de cache L1, L2, L3 des processeurs modernes.
Notons que ce cache interagit avec les techniques de compression de texture. Les textures sont en effet des images, qui sont donc compressées. Et elles restent compressées en mémoire vidéo, car les textures décompressées prennent beaucoup plus de place, entre 5 à 8 fois plus. Les textures sont décompressées lors des lectures : le processeur de shaders charge quelques octets, les décompresse, et utilise les données décompressées ensuite. Le cache s'introduit quelque part avant ou après la décompression.
On peut décompresser les textures avant de les placer dans le cache, ou laisser les textures compressées dans le cache. Tout est une question de compromis. Décompresser les textures dans le cache fait que la lecture dans le cache est plus rapide, car elle n'implique pas de décompression, mais le cache contient moins de données. À l'inverse, compresser les textures permet de charger plus de données dans le cache, mais rend les lectures légèrement plus lentes. C'est souvent la seconde solution qui est utilisée et ce pour deux raisons. Premièrement, la compression de texture est terriblement efficace, souvent capable de diviser par 6 la taille d'une texture, ce qui augmente drastiquement la taille effective du cache. Deuxièmement, les circuits de décompression sont généralement très rapides, très simples, et n'ajoutent que 1 à 3 cycles d'horloge lors d'une lecture.
Les anciens jeux vidéo ne faisaient que lire les textures, sans les modifier. Aussi, le cache de texture des cartes graphiques anciennes est seulement accessible en lecture, pas en écriture. Cela simplifiait fortement les circuits du cache, réduisant le nombre de transistors utilisés par le cache, réduisant sa consommation énergétique, augmentait sa rapidité, etc. Mais les jeux vidéos 3D récents utilisent des techniques dites de ''render-to-texture'', qui permettent de calculer certaines données et à les écrire en mémoire vidéo pour une utilisation ultérieure. Les textures peuvent donc être modifiées et cela se marie mal avec un cache en lecture seule.
Rendre le cache de texture accessible en écriture est une solution, mais qui demande d'ajouter beaucoup de circuits pour une utilisation somme toute peu fréquente. Une autre solution, plus adaptée, réinitialise le cache de textures quand on modifie une texture, que ce soit totalement ou partiellement. Une fois le cache vidé, les accès mémoire ultérieurs n'ont pas d'autre choix que d'aller lire la texture en mémoire et de remplir le cache avec les données chargées depuis la RAM. Les données de texture en RAM étant les bonnes, cela garantit l’absence d'erreur.
: Ces deux techniques peuvent être adaptées dans le cas où plusieurs caches de textures séparées existent sur une même carte graphique. Les écritures doivent invalider toutes les copies dans tous les caches de texture. Cela nécessite d'ajouter des circuits qui propagent l'invalidation dans tous les autres caches.
===Les caches de constante===
Un shader a besoin de certaines "constantes" pour faire son travail. Les constantes en question sont d'accès peu fréquent, qui se limite souvent à un accès au début de chaque instance de shader, guère plus. Au premier abord, dédier un cache à de telles constantes ne parait pas très utile, vu qu'elles ne semblent pas réutilisées. Mais c'est oublier un point important : toutes les instances du shader manipulent ces constantes, et il y en a souvent plusieurs qui s'exéceutn à tour de rôle, si ce n'est en même temps ! Pour profiter du partage des constantes entre instances d'un shader, les GPU incorporent des '''caches de constantes'''. Ainsi, quand un shader lit une donnée, elle est chargée dans le cache de constante, ce qui fait que les autres instances liront ces constantes depuis le cache et non depuis la VRAM.
Les caches de constante sont séparés des autres caches de données, car ce sont des données peu fréquemment utilisées, qui sont censées être évincées en priorité du cache de données, qui privilégie les données fréquemment lues/écrites. Avec un cache séparé, les constantes restent dans le cache. Au passage, ce cache de constante a des chances d'être partagé entre plusieurs cœurs, des cœurs différents ayant de fortes chances d’exécuter des instances différentes d'un même shader.
==Les caches généralistes==
Les GPUs récents contiennent des caches généralistes, qui ne sont spécialisés dans le rendu graphique. Leur existence se justifie par le fait que les GPU sont de plus en plus utilisés pour du calcul généraliste (scientifique, notamment), à savoir qu'ils exécutent des ''compute shaders'' qui manipulent des données arbitraires. Et de tels ''compute shaders'' sont parfois utilisés pour du rendu 3D, pour exécuter des algorithmes d'élimination des pixels cachés, ou des algorithmes de rendu assez complexes.
Les caches généralistes des GPU modernes ressemblent à ceux des CPU, avec une hiérarchie de caches. Pour rappel, les processeurs multicœurs modernes ont souvent trois à quatre niveaux de caches, appelés les caches L1, L2, L3 et éventuellement L4. Les GPU ont une organisation similaire, sauf que le nombre de cœurs est beaucoup plus grand que sur un processeur moderne.
* Pour le premier niveau, on a deux caches L1 par cœur/processeur : un cache pour les instructions et un cache pour les données.
* Pour le second niveau, on a un cache L2 qui peut stocker indifféremment données et instruction et qui est partagé entre plusieurs cœurs/processeurs.
* Le cache L3 est un cache partagé entre tous les cœurs/processeurs.
[[File:Partage des caches sur un processeur multicoeurs.png|centre|vignette|upright=2|Partage des caches sur un processeur multicoeurs]]
===Les caches d'instruction===
Les caches d'instruction des GPU sont adaptés aux contraintes du rendu 3D. Le principe du rendu 3D est d'appliquer un shader assez simple sur un grand nombre de données. Les shaders sont donc des programmes assez légers, qui ont peu d'instructions. Les caches d'instructions des GPU sont généralement assez petits, quelques dizaines ou centaines de kilooctets. Et malgré cela, il n'est pas rare qu'un ''shader'' tienne tout entier dans le cache d'instruction.
La seconde caractéristique est qu'un même programme s’exécute sur beaucoup de données. Il n'est pas rare que plusieurs processeurs de shaders exécutent le même ''shader''. Aussi, certains GPU partagent un même cache d’instruction entre plusieurs processeurs de ''shader'', comme c'est le cas sur les GPU AMD d'architecture GCN où un cache d'instruction de 32 kB est partagé entre 4 cœurs.
===Les caches de données===
Pour les caches de données, il faut savoir qu'un shader a peu de chances de réutiliser une donnée qu'il a chargé précédemment. Les processeurs de shaders ont beaucoup de registres, ce qui fait que si accès ultérieur à une donnée il doit y avoir, elle passe généralement par les registres. Cette faible réutilisation fait que les caches de données ne sont pas censé être très utiles. Il y a cependant des exceptions, qui expliquent que les cartes graphiques incorporent un cache de texture et un cache de sommet (pour le tampon de sommet).
Il faut noter que sur la plupart des cartes graphiques modernes, les caches de données et le cache de texture sont un seul et même cache. Même chose pour le cache de sommets, utilisé par les unités géométrique, qui est fusionné avec les caches de données. La raison est que une économie de circuits qui ne coute pas grand chose en termes de performance. Rappelons que les processeurs de shaders sont unifiés à l'heure actuelle, c'est à dire qu'elles peuvent exécuter pixel shader et vertex shader. Au lieu d'incorporer un cache de sommets et un cache de textures, autant utiliser un seul cache qui sert alternativement de cache de vertex et de cache de texture, afin d'économiser des circuits.
==La mémoire partagée : un ''local store''==
En plus d'utiliser des caches, les GPU modernes utilisent des ''local stores'', aussi appelés ''scratchpad memories''. Ce sont des mémoires RAM intermédiaires entre la RAM principale et les registres. Ces local stores peuvent être vus comme des caches, mais que le programmeur doit gérer manuellement. Dans la réalité, ce sont des mémoires RAM très rapides mais de petite taille, qui sont adressées comme n'importe quelle mémoire RAM, en utilisant des adresses directement.
[[File:Scratch-Pad-Memory.jpg|centre|vignette|upright=2.0|Scratch-Pad-Memory (SPM).]]
Sur les GPU modernes, chaque processeur de ''shader'' possède un unique ''local store'', appelée la '''mémoire partagée'''. Il n'y a pas de hiérarchie des ''local store'', similaire à la hiérarchie des caches.
[[File:Cuda5.png|centre|vignette|upright=2.0|Local stores d'un GPU.]]
La faible capacité de ces mémoires, tout du moins comparé à la grande taille de la mémoire vidéo, les rend utile pour stocker temporairement des résultats de calcul "peu imposants". L'utilité principale est donc de réduire le trafic avec la mémoire centrale, les écritures de résultats temporaires étant redirigés vers les local stores. Ils sont surtout utilisés hors du rendu 3D, pour les applications de type GPGPU, où le GPU est utilisé comme architecture multicœurs pour du calcul scientifique.
===L'implémentation des ''local store''===
Vous vous attendez certainement à ce que je dise que les ''local store'' sont des mémoires séparées des mémoires caches et qu'il y a réellement des puces de mémoire RAM distinctes dans les processeurs de ''shaders''. Mais en réalité, ce n'est pas le cas pour tous les ''local store''. Le dernier niveau de ''local store'', la mémoire partagée, est bel et bien une mémoire SRAM à part des autres, avec ses propres circuits. Mais les cartes graphiques très récentes fusionnent la mémoire locale avec le cache L1.
L'avantage est une économie de transistors assez importante. De plus, cette technologie permet de partitionner le cache/''local store'' suivant les besoins. Par exemple, si la moitié du ''local store'' est utilisé, l'autre moitié peut servir de cache L1. Si le ''local store'' n'est pas utilisé, comme c'est le cas pour la majorité des rendu 3D, le cache/''local store'' est utilisé intégralement comme cache L1.
Et si vous vous demandez comment c'est possible de fusionner un cache et une mémoire RAM, voici comment le tout est implémenté. L'implémentation consiste à couper le cache en deux circuits, dont l'un est un ''local store'', et l'autre transforme le ''local store'' en cache. Ce genre de cache séparé en deux mémoires est appelé un ''phased cache'', pour ceux qui veulent en savoir plus, et ce genre de cache est parfois utilisés sur les processeurs modernes, dans des processeurs dédiés à l'embarqué ou pour certaines applications spécifiques.
Le premier circuit vérifie la présence des données à lire/écrire dans le cache. Lors d'un accès mémoire, il reçoit l'adresse mémoire à lire, et détermine si une copie de la donnée associée est dans le cache ou non. Pour cela, il utilise un système de tags qu'on ne détaillera pas ici, mais qui donne son nom à l'unité de vérification : l''''unité de tag'''. Son implémentation est très variable suivant le cache considéré, mais une simple mémoire RAM suffit généralement.
En plus de l'unité de tags, il y a une mémoire qui stocke les données, la mémoire cache proprement dite. Par simplicité, cette mémoire est une simple mémoire RAM adressable avec des adresses mémoires des plus normales, chaque ligne de cache correspondant à une adresse. La mémoire RAM de données en question n'est autre que le ''local store''. En clair, le cache s'obtient en combinant un ''local store'' avec un circuit qui s'occupe de vérifier de vérifier les succès ou défaut de cache, et qui éventuellement identifie la position de la donnée dans le cache.
[[File:Phased cache.png|centre|vignette|upright=1.5|Phased cache]]
Pour que le tout puisse servir alternativement de ''local store'' ou de cache, on doit contourner ou non l'unité de tags. Lors d'un accès au cache, on envoie l'adresse à lire/écrire à l'unité de tags. Lors d'un accès au ''local store'', on envoie l'adresse directement sur la mémoire RAM de données, sans intervention de l'unité de tags. Le contournement est d'autant plus simple que les adresses pour le ''local store'' sont distinctes des adresses de la mémoire vidéo, les espaces d'adressage ne sont pas les mêmes, les instructions utilisées pour lire/écrire dans ces deux mémoires sont aussi potentiellement différentes.
[[File:Hydride cache - local store.png|centre|vignette|upright=2.0|Hydride cache - local store]]
Il faut préciser que cette organisation en ''phased cache'' est assez naturelle. Les caches de texture utilisent cette organisation pour diverses raisons. Vu que cache L1 et cache de texture sont le même cache, il est naturel que les caches L1 et autres aient suivi le mouvement en conservant la même organisation. La transformation du cache L1 en hydride cache/''local store'' était donc assez simple à implémenter et s'est donc faite facilement.
==La cohérence des caches sur un GPU==
Pour terminer ce chapitre, nous allons parler de la '''cohérence des caches'''. La cohérence des caches est un problème qui se manifeste à plusieurs niveaux, quand on parle d'un GPU : entre CPU et GPU, ou entre processeurs de shaders. Nous allons voir les deux cas l'un après l'autre.
===La cohérence des caches pour les transferts DMA===
Supposons que le CPU ait transféré les données dans la mémoire vidéo, avec un transfert DMA. Une situation bien précise pose problème : quand un transfert DMA écrase des données devenues inutiles, pour les remplacer par des données utiles. C'est très fréquent, les pilotes graphiques libèrent souvent de la mémoire vidéo pour la réallouer immédiatement après, afin de ne pas gaspiller de VRAM. Sans intervention du GPU, le remplacement des données aura été fait en mémoire vidéo, pas dans les caches du GPU. Et tout accès ultérieur au cache renverra la donnée écrasée.
[[File:Cache incoherence write.svg|centre|vignette|upright=2|Cohérence des caches avec DMA.]]
Pour éviter cela, le GPU invalide ses caches en cas de transfert DMA. Par invalider, on veut dire que le cache est réinitialisé, mis à zéro, il est rendu vierge de toute donnée. Ainsi, tout accès mémoire ultérieur se fera en mémoire RAM sans passer par le cache. Les données lues depuis la RAM seront ensuite copiées dans le cache, mais ce seront les données valides écrites après le transfert DMA. Le contenu du cache est alors reconstitué au fur et à mesure des accès mémoire. L'invalidation est automatique sur les anciens GPU, elle est réalisée par le processeur de commande. Sur les GPU modernes, elle est réalisée par le programmeur, comme on va le voir dans la section immédiatement suivante.
===La cohérence des caches entre CPU et GPU===
Dans ce qui suit, on suppose qu'il n'y a qu'une seule mémoire RAM, qui est partagée entre CPU et GPU, et sert à la fois de RAM et de mémoire vidéo. Il s'agit de ce que l'on appelle la mémoire unifiée. Elle est utilisée dans de nombreuses consoles de jeu, mais aussi avec les GPU intégrés, qui sont dans le processeur. La mémoire unifiée n'implique pas de transfert DMA entre CPU et GPU, vu qu'il n'y a qu'une seule RAM. Par contre, un problème différent du précédent peut survenir.
Le processeur n'écrit pas directement en mémoire RAM, mais dans son cache. Les écritures sont propagées en RAM avec un certain retard, quand les données sont évincées du cache pour faire de la place à de nouvelles données. Et même quand elles sont propagées en RAM, les écritures ne sont pas propagées dans les caches du GPU. Pour corriger cela, lorsque le processeur envoie des données au GPU, il force le GPU à invalider ses caches. Il envoie une commande dédiée pour, qui précède les commandes liées au rendu 2D/3D/autres.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée]]
Au niveau du processeur, le processeur doit écrire les données dans la mémoire RAM, avant de faire le transfert DMA. Mais la présence de caches pose problème : les écritures peuvent être interceptées par le cache et ne pas être propagées en RAM. Pour éviter cela, les processeurs modernes marquent des blocs de mémoire comme "non-cacheables", à savoir que toute lecture/écriture dedans se fait sans passer par le cache. C'est une fonctionnalité très importante pour communiquer avec les périphériques. Pour les GPU dédiés/soudés, cela a un lien avec la mémoire vidéo mappée en mémoire. Plus haut, nous avions dit que la mémoire vidéo est visible dans l'espace d'adressage du processeur, à savoir qu'un bloc de mémoire est détourné pour adresser non pas la RAM, mais la mémoire vidéo. Et bien ce bloc de mémoire entier est marqué comme étant non-cacheable.
[[File:Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU avec mémoire unifiée, mécanismes]]
La situation peut être optimisée sur les GPU intégrés. Si le GPU est conçu pour, il n'y a pas besoin de marquer les données comme non-cacheables. Le cas le plus simple est celui où le CPU et le GPU partagent leur cache L3/L4. Dans ce cas, il n'y a qu'un seul cache L3/L4 qui ne contient qu'une seule copie valide, écrite par le CPU et lue par le GPU. Il faut juste garantir que la donnée soit lue par le GPU depuis le L3, mais c'est là une question d'inclusivité du cache, qui ne nous concerne pas ici. Si le CPU et le GPU ne partagent pas de cache, il suffit que le GPU puisse lire les caches du CPU. C'est la méthode utilisée sur l'APU Trinity d'AMD.
[[File:Cohérence des caches entre CPU et GPU intégré.png|centre|vignette|upright=2|Cohérence des caches entre CPU et GPU intégré]]
===La cohérence des caches entre processeurs de shaders===
Pour terminer, il faut voir la cohérence des caches entre processeurs de shaders. Une carte graphique moderne est, pour simplifier, un gros processeur multicœurs auquel on aurait rajouté des ROPs, les circuits de la rastérisation et les unités de textures. Il n'est donc pas étonnant que les problèmes rencontrés sur les processeurs multicœurs soient aussi présents sur les GPU, la cohérence des caches ne fait pas exception.
Pour simplifier les explications, nous allons partir du principe que chaque processeur de shaders a son propre cache de données. Prenons deux processeur de shaders qui ont chacun une copie d'une donnée dans leur cache. Si un processeur de shaders modifie sa copie de la donnée, l'autre ne sera pas mise à jour. L'autre processeur manipule donc une donnée périmée : il n'y a pas cohérence des caches.
[[File:Cohérence des caches.png|centre|vignette|upright=2|Cohérence des caches]]
La réalité est cependant plus complexe, dans le sens où il n'y a souvent pas un cache par processeur de shaders, mais une hiérarchie de cache assez complexe, avec un cache L1 par processeur de shaders, un cache L2 partagé entre plusieurs processeur de shaders, des caches partagés entre tous les processeur de shaders, etc. Certains GPU partagent leur cache L1 d’instructions entre plusieurs processeur de shaders, d'autres non. Mais le principe reste valide, tant qu'un cache n'est pas partagé entre tous les processeurs de shaders : un cache peut contenir une donnée invalide, à savoir qu'elle a été modifiée dans le cache d'un autre processeur de shaders.
Pour corriger ce problème, les ingénieurs ont inventé des '''protocoles de cohérence des caches''' pour détecter les données périmées et les mettre à jour. Mais autant ces techniques sont faisables sur des CPU avec un nombre limité de cœurs, autant elles sont impraticables avec un GPU contenant une centaine de cœurs. Heureusement, la cohérence des caches est un problème bien moins important sur les GPU que sur les CPU. En effet, le rendu 3D implique un parallélisme de données : des processeurs/cœurs différents sont censés travailler sur des données différentes. Il est donc rare qu'une donnée soit traitée en parallèle par plusieurs cœurs, et donc qu'elle soit copiée dans plusieurs caches.
En conséquence, les GPU se contentent d'une cohérence des caches assez light, gérée par le programmeur. Si jamais une opération peut mener à un problème de cohérence des caches, le programmeur doit gérer cette situation de lui-même. Pour cela, les GPU supportent des instructions machines spécialisées, qui vident les caches. Par vider les caches, on veut dire que leur contenu est rapatrié en mémoire RAM, et qu'ils sont réinitialisés. Les accès mémoire qui suivront l'invalidation trouveront un cache vide, et devront recharger leurs données depuis la RAM. Ainsi, si une lecture/écriture peut mener à un défaut de cohérence problématique, le programmeur insère une instruction qui invalide le cache, avant l'accès mémoire potentiellement problématique. Ainsi, on garantit que la donnée chargée/écrite est lue depuis la mémoire vidéo et donc qu'il s'agit d'une donnée correcte.
Elle est utilisée pour supporter les techniques de ''render-to-texture'', pour dessiner l'image finale dans une texture (pour y appliquer un filtre de post-traitement, par exemple). Les opérations de ''render-to-texture'' étant assez rares, il vaut mieux ne pas rendre les caches de texture accessibles en écriture. L'invalidation du cache au besoin est alors parfaitement adapté. Les autres caches du GPU sont gérés avec le même principe. Pour les caches généralistes, certains GPU modernes commencent à implémenter des méthodes plus élaborées de cohérence des caches.
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shader VLIW et DirectX 9
| prevText=Les processeurs de shader VLIW et DirectX 9
| next=La mémoire unifiée et la mémoire vidéo dédiée
| netxText=La mémoire unifiée et la mémoire vidéo dédiée
}}{{autocat}}
t4fygt49dji8fmudt557cfep2sgguk5
Les cartes graphiques/Le rendu d'une scène 3D : concepts de base
0
79234
763347
763205
2026-04-09T18:22:34Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763347
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* L''''illumination diffuse''' vient du fait que la surface d'un objet diffuse une partie de la lumière qui lui arrive dessus dans toutes les directions. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
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. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
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, il est modifié 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.
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
0ap1ntfshn7hpo4l3rl3e0famcnb56m
763348
763347
2026-04-09T18:44:36Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763348
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* L''''illumination diffuse''' vient du fait que la surface d'un objet diffuse une partie de la lumière qui lui arrive dessus dans toutes les directions. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
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. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
6432xi9loeccmd11tw9atf3bxehb06e
763349
763348
2026-04-09T18:44:43Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763349
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Les illuminations diffuse et spéculaire===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* L''''illumination diffuse''' vient du fait que la surface d'un objet diffuse une partie de la lumière qui lui arrive dessus dans toutes les directions. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
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. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
l8vv8k0fv50k91x6xpuzor6a01lfa00
763350
763349
2026-04-09T18:49:55Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763350
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===Les illuminations diffuse et spéculaire===
[[File:Lambert2.gif|vignette|Différence entre réflexion diffuse et spéculaire.]]
Le calcul exact de l'illumination de chaque sommet demande de calculer plusieurs illuminations indépendantes. En plus de la lumière ambiante, il faut ajouter la lumière réfléchie par la surface du modèle 3D. Il en existe plusieurs sous-types, les deux principaux étant les suivants :
* L''''illumination spéculaire''' est la lumière réfléchie via la réflexion de Snell-Descartes, dont la couleur dépend de la couleur de la surface réfléchissante.
* L''''illumination diffuse''' vient du fait que la surface d'un objet diffuse une partie de la lumière qui lui arrive dessus dans toutes les directions. La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions.
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. Mais même sur ces derniers, la réflexion spéculaire n'est pas parfaite. Elle ne l'est que pour les miroirs parfaits. La lumière spéculaire forme une sorte de lobe, où la lumière est très intense dans la direction idéale, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
tjfw1x8r2mgatopnqqajs7tyr76gcni
763351
763350
2026-04-09T19:09:04Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763351
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===Les illuminations diffuse et spéculaire===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>^alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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. Bien sur, cela ne vaut que pour une partie de la lumière, il y a une réflexion prédominante dans la direction de la réflexion parfaite.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
97akduj6mrxzuhkc85tkdd11h2e4skl
763352
763351
2026-04-09T19:09:19Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763352
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===Les illuminations diffuse et spéculaire===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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. Bien sur, cela ne vaut que pour une partie de la lumière, il y a une réflexion prédominante dans la direction de la réflexion parfaite.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
q2ddsdp2glol821vjloebv2w2ae18td
763353
763352
2026-04-09T19:09:56Z
Mewtow
31375
/* Les illuminations diffuse et spéculaire */
763353
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===La réflexion de la lumière sur la surface===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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. Bien sur, cela ne vaut que pour une partie de la lumière, il y a une réflexion prédominante dans la direction de la réflexion parfaite.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
lxvf78273ettvqde0523151ybzehi5e
763354
763353
2026-04-09T19:40:42Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763354
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===La réflexion de la lumière sur la surface===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Bien sur, cela ne vaut que pour une partie de la lumière, il y a une réflexion prédominante dans la direction de la réflexion parfaite. Si on regarde la réflexion sur la plupart des matériaux, on voit quelque chose qui ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Specular highlight.jpg|centre|vignette|upright=2|Specular highlight]]
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
L'illumination diffuse et spéculaire sont calculées séparément et additionnées ensemble pour donner l'illumination finale du point de surface. Chaque composante rouge, bleu, ou verte de la couleur est traitée indépendamment des autres, ce qui donne une scène 3D coloriée.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, l'illumination spéculaire dépend de l'angle entre la caméra, la normale, et la direction du regard.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
88rf5v895uo2muhc2rk17hqf9agfi8f
763356
763354
2026-04-09T19:43:04Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763356
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{N, L} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===La réflexion de la lumière sur la surface===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
k45edlajssx9ypb600cemw0pnhytxe3
763357
763356
2026-04-09T19:45:54Z
Mewtow
31375
/* Le terme d'illumination géométrique */
763357
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===La réflexion de la lumière sur la surface===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
La couleur diffuse ne dépend pas vraiment de l'orientation de la caméra par rapport à la surface. Elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale). Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
g6d45oxrlefi3mludcj0sv9hczek6or
763358
763357
2026-04-09T19:50:50Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763358
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===Le terme d'illumination géométrique===
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. L'éclairage attribue à chaque sommet une '''illumination''', à savoir sa luminosité. L'illumination d'un sommet est définie par un niveau de gris. Plus un sommet a une illumination importante, plus il a une couleur proche du blanc. Et inversement, plus son illumination est faible, plus il est proche du noir.
L'illumination d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. Mathématiquement, cela se traduit assez simplement, par le produit de deux termes : l'intensité de la lumière qui arrive sur la surface, une fonction qui indique comment la surface réfléchit la lumière. 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.
Intuitivement, la lumière qui arrive sur la surface 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 sera étalée sur une surface plus grande, si la lumière 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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière reçue dépend donc de l'angle avec la verticale. Pour cela, les calculs d'éclairage ont besoin de connaitre, en chaque point de la surface, quelle est sa verticale. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir une normale 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, il est modifié 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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.
===La réflexion de la lumière sur la surface===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Parce qu'elle est particulièrement "rugueuse" au niveau microscopique. On a alors le ''material'' le plus simple qui soit, appelé un ''diffuse material''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur diffuse ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale).
Un bon moyen pour représenter cette réflexion diffuse est simplement de
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le produit scalaire de deux vecteurs===
Les calculs de lumière utilisent les angles entre ces vecteurs. Et plus précisémment, le cosinus de ces angles. Or, les calculs trigonométriques sont très gourmands pour le matériel. 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
232ux9kk4zm9qfsloj6sblj3qrek7j1
763359
763358
2026-04-09T19:55:43Z
Mewtow
31375
/* L'éclairage d'une scène 3D */
763359
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Parce qu'elle est particulièrement "rugueuse" au niveau microscopique. On a alors le ''material'' le plus simple qui soit, appelé un ''diffuse material''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur diffuse ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale).
Un bon moyen pour représenter cette réflexion diffuse est simplement de
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents. L'illumination diffuse dépend juste de l'angle entre la normale et l'angle L (sommet-lumière). L'angle entre N et L donne la manière dont la surface est penchée par rapport à la surface lumineuse : plus elle est penchée, plus la lumière est diffusée et moins l'illumination diffuse est intense. L'angle en question est noté <math>omega</math>.
Ici, on veut le cosinus de l'angle entre normale et vecteur L. Les deux vecteurs ont une longueur de 1 par définition, donc ce cosinus et le produit scalaire sont identiques. Le cosinus est donc calculé en faisant le produit scalaire entre la normale de la surface et le vecteur L. En combinant les deux équations précédentes, on a :
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
p0py5bw0zxcygylqzgzgfjroan4vprg
763360
763359
2026-04-09T19:56:12Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763360
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Parce qu'elle est particulièrement "rugueuse" au niveau microscopique. On a alors le ''material'' le plus simple qui soit, appelé un ''diffuse material''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur diffuse ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement de l'angle entre le rayon de lumière et la verticale de la surface (sa normale).
Un bon moyen pour représenter cette réflexion diffuse est simplement de
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Pour rendre compte de ce phénomène, une approximation découpe la lumière réfléchie en deux-types : une réflexion diffuse
* Une '''réflexion spéculaire''' qui est très très proche de la réflexion parfaite.
* Une '''réflexion diffuse''' qui réfléchit la lumière dans toutes les directions.
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. La réflexion spéculaire n'est pas exactement la réflexion parfaite, mais elle réfléchit la lumière dans des directions très proches. Elle forme une sorte de lobe, où la lumière est très intense dans la direction parfaite, et s'atténue très vite quand on s'en éloigne. Le tout sera certainement plus clair avec l'illustration ci-dessous.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
L'attribution d'une illumination à chaque sommet fait que la scène 3D est éclairée. Mais on peut aussi aller plus loin et colorier les sommets. Ainsi, l'éclairage ne fasse pas que rendre la scène 3D en niveaux de gris, mais que les niveaux de luminosité sont calculés indépendamment pour chaque couleur RGB. L'éclairage calcule alors une couleur ambiante, une couleur diffuse, et une couleur spéculaire.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents.
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
579xnepvx4xi1533frkb4nl3s801wnv
763361
763360
2026-04-09T20:02:57Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763361
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur d'un sommet ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement du terme géométrique et de la couleur diffuse. Le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Un '''''Phong material'''' cumule à la fois une réflexion diffuse et une réflexion spéculaire. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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. Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents.
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
pe7gaf1qo0iai15ye004h7e9f7usw2d
763362
763361
2026-04-09T20:03:15Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763362
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur d'un sommet ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement du terme géométrique et de la couleur diffuse. Le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
===Les données nécessaires pour les algorithmes d'illumination===
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents.
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
e8rcwyw6naih7wu6maktix4h1sxtpwv
763363
763362
2026-04-09T20:03:47Z
Mewtow
31375
/* Les données nécessaires pour les algorithmes d'illumination */
763363
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur d'un sommet ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement du terme géométrique et de la couleur diffuse. Le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
===Le calcul des couleurs par un algorithme d'illumination de Phong===
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents.
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
03olwvcem7u4uaarp2w2bozk3yvf5ys
763364
763363
2026-04-09T20:03:59Z
Mewtow
31375
/* Le calcul des couleurs par un algorithme d'illumination de Phong */
763364
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Pour cela, la solution la plus simple est de lui donner une '''couleur diffuse''', qui est multipliée par la lumière incidente. Rien de plus, rien de moins. La couleur d'un sommet ne dépend pas de l'orientation de la caméra par rapport à la surface, elle dépend uniquement du terme géométrique et de la couleur diffuse. Le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times I \times (\vec{N} \cdot \vec{L})</math>
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
Le résultat ressemble à ce qui est illustré ci-dessous. Une couleur diffuse, couplée à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''.
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
Par contre, la réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense.
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul des illuminations ambiantes, spéculaire et diffuse est le fait d'algorithmes plus ou moins compliqués. Ils sont très nombreux, au point où on ne pourrait pas en faire la liste. Les algorithmes varient beaucoup d'un moteur de jeu à l'autre, d'une carte graphique à l'autre. Les plus simples se décrivent en quelques équations, les algorithmes les plus complexes prennent un chapitre à eux seuls dans un livre spécialisé.
L'algorithme d’illumination a besoin de plusieurs informations, certaines étant des nombres, d'autres des vecteurs, d'autres des angles. Tous les algorithmes d'éclairage impliquent de faire des calculs trigonométriques dans l'espace, de déterminer des angles, des distances, et bien d'autres choses.
Pour la source de lumière, il faut préciser l'intensité de la source de lumière et sa couleur. Pour les sommets, il faut préciser la '''couleur du sommet''', qui donne la couleur de la surface échantillonnée sur le sommet. Il y a précisément trois couleurs : une pour la lumière ambiante, une pour la lumière diffuse, et une pour la lumière spéculaire. En théorie, les deux premières couleurs sont censées être identiques, mais il est possible de configurer les deux séparément, quelle qu'en soit la raison.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Pour ce qui est des sommets, les calculs d'éclairage demandent de connaitre plusieurs vecteurs. Pour une source de lumière ponctuelle, les vecteurs sont illustrés ci-contre. En voila la liste :
* La '''normale''', un vecteur perpendiculaire à la surface de l'objet
* Le vecteur qui part de la caméra et pointe vers le sommet (noté w dans le schéma ci-dessous).
* Le vecteur qui part de la source de lumière et le point de surface (noté L dans le schéma ci-dessous).
: Pour une lumière directionnelle, le dernier vecteur est remplacé par le vecteur direction, qui est prédéfinit pour chaque source de lumière. Pas besoin de le calculer, il ne dépend pas du sommet considéré.
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
Les trois vecteurs précédents suffisent pour calculer la lumière diffuse. Mais pour calculer la lumière spéculaire, il faut ajouter un quatrième vecteur. Il a la même direction que la lumière réfléchie par la surface et se calcule avec la loi de Snell-Descartes de la réflexion. Ce vecteur est noté R dans le schéma ci-contre.
Pour résumer, les quatre vecteurs sont les suivants :
* la normale N est un vecteur perpendiculaire à la surface, au sommet ;
* le vecteur L qui connecte le sommet avec la source de lumière ;
* le vecteur V qui connecte la caméra au sommet ;
* le vecteur R qui se calcule à partir du vecteur L et N, qui représente le trajet d'un rayon lumineux réfléchit par la surface au sommet, calculé avec les lois de Snell-Decartes.
À partir de ces informations, divers algorithmes peuvent éclairer une scène. Dans ce qui va suivre, nous allons voir l''''algorithme d'illumination de Phong''', la méthode la plus utilisée dans le rendu 3D. S'il n'est peut-être pas l'algorithme le plus utilisé, vous pouvez être certain que c'est au moins une version améliorée ou un autre algorithme proche mais plus poussé qui l'est à sa place.
L'illumination spéculaire et diffuse sont calculées à partir des vecteurs précédents.
Pour calculer la lumière spéculaire, il y a plusieurs manières de faire. Celle utilisée par l'éclairage de Phong utilise l'angle entre v et R, et précisémment son cosinus. le calcul exact est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})</math>
Enfin, l'illumination ambiante d'un point de surface s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
c125waw1whje9mu16hv029py954k1au
763365
763364
2026-04-09T20:21:11Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763365
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v}^{ \alpha})</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter l'illumination ambiante d'un point de surface, qui s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
hc72bkgrnazeklyt5xmjl1fmjq8qeh6
763366
763365
2026-04-09T20:21:43Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763366
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter l'illumination ambiante d'un point de surface, qui s'obtient en multipliant la lumière ambiante par la couleur du sommet. Pour rappel, la couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la couleur ambiante. En la multipliant avec la lumière ambiante, on a l'illumination ambiante. Les deux sont des constantes pré-calculées par les concepteurs du jeu vidéo ou du rendu 3D.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
479gyezu0mmrujqaudhm1zp9g88g6v7
763367
763366
2026-04-09T20:23:40Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763367
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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.
Les sources de lumière ont une couleur, la couleur de la lumière émise, et émettent une intensité lumineuse codée par un entier.
===La lumière incidente : le terme géométrique===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
6m5sdemr941oh99rg3codxcf0dl34mh
763368
763367
2026-04-09T20:24:07Z
Mewtow
31375
/* Les sources de lumière et les couleurs associées */
763368
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
8cb1hrlt31u49b5cwfiy6fhai920ezn
763369
763368
2026-04-09T20:28:46Z
Mewtow
31375
/* La lumière incidente : le terme géométrique */
763369
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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 couleur d'un sommet dépend de deux choses : la lumière qui lui arrive dessus, comment il réfléchit cette lumière. La lumière qui arrive dessus est appelée la '''lumière incidente'''.
Mathématiquement, cela se traduit assez simplement, par 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
sjj57u3vw8b9sq3amknbr1g228p1vn2
763370
763369
2026-04-09T20:30:17Z
Mewtow
31375
/* La lumière incidente : le terme géométrique */
763370
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
6tuj3luov9tah94toblglufakpof50x
763371
763370
2026-04-09T20:30:28Z
Mewtow
31375
/* La lumière incidente : le terme géométrique */
763371
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
n12x3azlvpepgeub9y591gg77r9trh2
763372
763371
2026-04-09T20:31:22Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763372
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. La couleur diffuse est simplement multipliée par la lumière incidente, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. La couleur d'un sommet dépend alors uniquement du terme géométrique et de la couleur diffuse.
Si le terme géométrique est calculé en utilisant un produit scalaire, ce qui 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
b2qz68w45zkbpikmhsxr4uhjlak5g0m
763373
763372
2026-04-09T20:32:13Z
Mewtow
31375
/* La réflexion de la lumière sur la surface */
763373
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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'acléirage 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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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éges vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténué assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élevé ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. l'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particulier d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surface planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calculs d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
odue9vrcx3c3wmoahjw1aqmwbnznz25
763374
763373
2026-04-09T20:34:08Z
Mewtow
31375
/* L'éclairage d'une scène 3D */
763374
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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.
Il est possible d'ajouter une étape d'éclairage dans la phase de traitement de la géométrie. Elle attribue une illumination/couleur à chaque triangle ou à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, qui ne suffit pas à elle seule à éclairer la scène comme voulu.
L''''éclairage par pixel''' (''per-pixel lighting''), est calculé 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. Même des algorithmes d'éclairage par pixel très simples demandent une intervention des pixels shaders. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
===Les sources de lumière et les couleurs associées===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Maintenant que l'on sait comment est calculé l'illumination d'un point de surface, passons maintenant aux algorithmes d'éclairage proprement dit. C’est une bonne chose de savoir comment les points de surface (sommets ou triangles) sont éclairés, mais rappelons que ce sont des pixels qui s'affichent à l'écran. L'étape d'éclairage réalisée par la géométrie peut calculer la luminosité/couleur d'un sommet/triangle, mais pas celle d'un pixel. Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. Les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particuliers d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
16nyxw2qo6mxjzgy748kigpbl9gh3x5
763375
763374
2026-04-09T20:42:32Z
Mewtow
31375
/* L'éclairage d'une scène 3D */
763375
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant 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. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
Pour cela, il faut déterminer la luminosité d'un pixel à partir de la luminosité/couleur des sommets du triangle associé. Pour cela, il existe plusieurs algorithmes qui font ce calcul. Les trois plus connus sont appelés l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particuliers d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
sn2bn94gk1ih8hu8k3sepzy9ztxkf35
763376
763375
2026-04-09T20:43:58Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763376
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant 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. Autant dire que cette distinction a été importante dans l'évolution du matériel graphique et l'introduction des ''shaders''. Mais pour comprendre cela, il faut voir comment fonctionnent les algorithmes d'éclairage et voir comment le hardware peut aider à les implémenter.
En général, l'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3 (l'un des premiers jeux à utiliser ce genre d'éclairage), en raison de sa meilleure qualité, les ordinateurs actuels étant assez performants. Mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particuliers d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
8qto7rf3ntdd52lji8fvmq8al3ou1j0
763377
763376
2026-04-09T20:44:44Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763377
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant 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. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
L''''éclairage de Phong''' est différent des précédents dans le sens où il 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, seuls les vecteurs adéquats le sont. C'est notamment le cas de la normale, qui est calculée pour chaque sommet. La normale est alors calculée pour chaque pixel, par interpolation des normales des sommets du triangle. Puis, l'algorithme d'illumination de Phong est effectué pour chaque normale interpolée.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
Pour simplifier, l'éclairage plat calcule la couleur triangle par triangle, l'éclairage de Gouraud calcule la couleur sommet par sommet, et l'éclairage de Phong calcule la couleur pixel par pixel. La différence entre les trois algorithmes se voit assez facilement. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particuliers d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
6lhy7hungi8es6vz871ym3wf23hj57m
763378
763377
2026-04-09T20:48:20Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763378
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant 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. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
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. A 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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
L'éclairage de Gouraud et l'éclairage plat sont des cas particuliers d''''éclairage par sommet''' (''vertex lighting''), où l'éclairage est calculé sur la géométrie d'une scène 3D. L'éclairage par pixel demande lui d'agir après l'étape de rastérisation, avant ou après l'unité de textures. Les trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
4lxd0mjvu4wozyav77r3gyb7riylj7a
763379
763378
2026-04-09T20:48:58Z
Mewtow
31375
/* Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel */
763379
wikitext
text/x-wiki
Le premier jeu à utiliser de la "vraie" 3D 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==
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===
[[File:Dolphin triangle mesh.png|vignette|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.
[[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]]
: En général, il est possible de créer un modèle 3D avec autre chose que des triangles ou des quadrilatères, avec des polygones concaves, des courbes de Béziers, et bien d'autres techniques. Mais ces solutions sont peu pratiques et plus complexes.
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.
Un segment qui connecte une paire de sommets s'appelle une '''arête''', comme en géométrie élémentaire. Plusieurs arêtes délimitent une surface fermée, celle-ci est appelée une ''face'', ou encore une '''primitive'''.
[[File:Mesh overview.svg|centre|vignette|upright=2.5|Surface représentée par ses sommets, arêtes, triangles et polygones.]]
Les API 3D supportent des primitives assez diverses. Elles gèrent au minimum les points, les lignes et les triangles. Elles gèrent éventuellement les ''triangle-strip'' et ''triangle-fan''. Aujourd'hui, OpenGL et DirectX ne gèrent plus le rendu avec des ''quads'' est aujourd’hui tombé en désuétude, mais il est parfois supporté par les cartes graphiques actuelles, bien que ce soit souvent par émulation (un ''quad'' est rendu avec deux triangles). Pour information, voici les primitives gérées par les premières versions d'OpenGL :
[[File:GeometricPrimitiveTypes.png|centre|vignette|upright=2.5|Primitives supportées par OpenGL.]]
Précisons cependant que le support d'une primitive par une API 3D ne signifie pas que la carte graphique supporte ces primitives. Il se peut que les primitives soient découpées en triangles par la carte graphique lors de l'exécution, alors qu'elles sont supportées par l'API 3D.
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.
===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 au minimum par :
* une position ;
* par la direction du regard (un vecteur).
A 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. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''.
[[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]]
[[File:ViewFrustum.svg|vignette|upright=1|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.
===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.
[[File:Texture+Mapping.jpg|centre|vignette|upright=2|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 quelques consoles de jeu. La 3DO, la PS1, la Sega Saturn, utilisaient ce genre de rendu. Avec ce genre de rendu, les textures ne sont pas plaquer sur les objets, mais sont en fait écrites directement dans le ''framebuffer'', après avoir été tournées et redimensionnés. Il s'agit d'un rendu très particulier que nous aborderons dans ce cours dans des chapitres dédiés. De nos jours, on utilise uniquement la technique de placage de texture inverse, qui sera décrite plus bas.
===La différence entre rastérisation et lancer de rayons===
[[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]]
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 effectue beaucoup de traitements différents, mais dont le principal est d'attribuer chaque triangle à un pixel donné de l'écran.
[[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]]
L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du 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.
Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. 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==
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===
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..
[[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|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).
[[File:View transform.svg|centre|vignette|upright=2|É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===
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==
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''===
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.
[[File:View frustum culling.svg|centre|vignette|upright=1|''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'''.
[[File:Occlusion culling example PL.svg|centre|vignette|''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===
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'''.
[[File:Polygons cross.svg|vignette|Polygons cross]]
[[File:Painters problem.svg|vignette|Painters problem]]
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===
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. Au fur et à mesure que les triangles sont rastérisés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra.
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.
[[File:Z-buffer.svg|centre|vignette|upright=2.0|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 :
[[File:Z-fighting.png|centre|vignette|Z-fighting]]
==La rastérisation==
L'étape de 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.
L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que 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===
[[File:Obj lineremoval.png|vignette|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.
{|
|[[File:Maze war.jpg|vignette|Maze war]]
|[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|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. A 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. A 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===
[[File:MiniFighter.png|vignette|upright=1|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/''quad''aprè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é'''.
[[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|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 :
* [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.]
===Le placage de textures direct===
Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D.
L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle 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. Et le ''sprite'' doit être déformé de la même manière que le ''quad''.
L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un 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''.
[[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]]
La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. 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.
: Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran.
Un autre point 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, 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''.
{|class="wikitable"
|-
! 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.
[[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]]
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 placage de textures inverse===
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.
[[File:Texture Mapping example.png|centre|vignette|upright=2|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.
[[File:UVMapping.png|centre|vignette|upright=2|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.
[[File:01 3D-Rasterung-a.svg|vignette|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. L'implémentation simplifiée est que l'on a des unités géométriques, une unité de rastérisation, un circuit de placage de textures et enfin un ROP pour gérer les opérations finales. 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.
==L'éclairage d'une scène 3D==
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===
[[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]]
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 deux principaux.
* Les plus simples sont les '''sources ponctuelles''' de lumière, de simples 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.
* 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.
Il existe des sources de lumière ponctuelles qui émettent de manière égale dans toutes les directions. Mais il existe aussi des sources de lumière ponctuelles qui é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. La direction privilégiée est un vecteur, notée v dans le schéma du dessous.
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. La lumière rentre par la fenêtre et rebondit sur les murs, le plafond, le sol, et éclaire toute la pièce. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D.
[[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]]
De nos jours, il existe des techniques pour simuler cet éclairage ambiant d'une manière assez crédible. Mais 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===
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, 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.
: <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math>
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.
[[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]]
La lumière incidente dépend donc de l'angle avec la verticale. 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.
[[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]]
La lumière arrivante est définie par un vecteur, qui arrive d’où vient la lumière, qui sera noté L. La lumière qui arrive sur la surface dépend de l'angle entre la normale et ce 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 :
: <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math>
Le terme <math>I \times \cos{N, L}</math> 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===
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é <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante :
: <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, 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 :
: <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math>
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===
[[File:Ray Diagram 2.svg|vignette|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. Dans le détail, la lumière arrive sur la surface avec un certain angle d’incidence <math>\alpha</math>, par rapport à la normale. La lumière est réfléchie avec un angle de réflexion, par rapport à la normale. Les lois de l'optique géométrique nous disent que 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 n'est pas parfaitement lisse. Elle 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'''.
{|
|-
|[[File:Haze-Reflection-2.png|centre|vignette|upright=1.2|Réflexion diffuse.]]
|[[File:Diffuse reflection.svg|centre|vignette|upright=1|Réflexion diffuse.]]
|}
Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Reste à calculer la réflexion diffuse. Il y a plusieurs algorithmes pour cela, mais la solution la plus simple est de donner une '''couleur diffuse''' à chaque sommet. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. 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 ;
* <math>K_d</math> est la couleur diffuse.
: <math>\text{Illumination diffuse} = K_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math>
[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|Différence entre réflexion diffuse et spéculaire.]]
Maintenant, il faut savoir que de nombreux matériaux ne se limitent pas à de la réflexion diffuse. Ils ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, mais réfléchit la lumière dans des directions très proches. 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'''''.
[[File:Reflection models.svg|centre|vignette|upright=2.0|Illustration de la dispersion de la lumière directionnelle : les deux premiers sont des exemples d'illumination diffuse, calculés avec des algorithmes différents. Le troisième cas est l'illumination spéculaire.]]
[[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]]
La réflexion spéculaire dépend de l'angle entre la caméra et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Évidemment, cela demande de calculer un vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. La réflexion spéculaire est une fonction qui dépend de l'angle entre ce vecteur R, et la direction du regard notée V. Et il faut aussi tenir compte du terme géométrique.
: <math>\text{Illumination spéculaire} = \left[ I \times (\vec{N} \cdot \vec{L}) \right] \times f(\vec{R} \cdot \vec{V}) </math>
La fonction en question peut être très compliquée, mais le cas classique est celui des ''Phong material''. Un '''''Phong material''''' cumule à la fois une réflexion diffuse et une réflexion spéculaire, la réflexion spéculaire étant calculée avec une formule mathématique bien précise. Les deux sont présentes en des proportions différentes, qu'on peut configurer pour chaque ''material''. 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.
Le calcul exact de la lumière spéculaire avec ce genre de ''material'' est un peu complexe. Il a besoin d'une '''couleur spéculaire''', qui est équivalente à la couleur diffuse, mais pour la réflexion spéculaire. Ensuite, il y a besoin de connaitre l'angle entre R et V. Simplement multiplier les deux donnerait un mauvais résultat, car la lumière spéculaire ne serait pas atténuée assez vite, quand on s'éloigne de la direction de réflexion maximale. Aussi, on élève ce cosinus à une certaine puissance. L'exposant dépend du ''matérial'' considéré, c'est un paramètre qu'on peut faire varier à loisir. Le résultat final est le suivant :
: <math>\text{Illumination spéculaire} = K_s \times I \times (\vec{R} \cdot \vec{v})^{ \alpha}</math>
: Le terme géométrique est en réalité pris en compte implicitement, je ne rentre pas dan le détail.
A cela, il faut rajouter la lumière ambiante, qui est simplement obtenue en multipliant l'intensité de la lumière ambiante par la couleur du sommet. La couleur utilisée est censée être la couleur diffuse, mais il est possible d'utiliser une couleur différente, appelée la '''couleur ambiante'''.
: <math>\text{Illumination ambiante} = K_a \times I_a</math> avec <math>K_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante.
En additionnant ces trois sources d'illumination, on trouve :
: <math>\text{Illumination totale} = K_a \times I_a + I \times \left[ K_d \times (\vec{N} \cdot \vec{L}) + K_s \times (\vec{R} \cdot \vec{v}) \right]</math>
[[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]]
===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel===
Avec tout ce qui a été dit précédemment, l'éclairage était calculé pour chaque sommet. Il attribuait une illumination/couleur à chaque sommet de la scène 3D. L'éclairage obtenu est appelé de l''''éclairage par sommet''', ou ''vertex lighting'', terme qui désigne toute technique où l'éclairage est calculé pour chaque sommet d’une scène 3D. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant 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. L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable.
La distinction de l'éclairage par pixel et par sommet est souvent complété avec une troisième possibilité : l''''éclairage par triangle'''. L'idée est de donner une couleur/illumination par triangle, et non par sommet. Les trois possibilités sont souvent appelées l''''éclairage plat''', l''''éclairage de Gouraud''', et l''''éclairage de Phong'''. Notons que ce dernier n'est pas identique à l'éclairage de Phong vu précédemment.
[[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]]
L''''éclairage plat''' calcule l'éclairage triangle par triangle. Typiquement, l'algorithme d'éclairage part de la normale du triangle, puis effectue les calculs d'éclairage à partir de cette normale. La normale est fournie pour chaque triangle, directement dans le modèle 3D, de même que chaque triangle a un coefficient de réflexion ambiante/spéculaire/diffuse. L'algorithme applique ensuite l’algorithme d'illumination triangle par triangle, ce qui fait que chaque triangle se voit attribuer une couleur, puis l'unité de rastérisation applique cette couleur sur tous les pixels associés à ce triangle.
L''''éclairage de Gouraud''' calcule l'éclairage sommet par sommet. Tous les sommets se voient attribuer une illumination, puis l'algorithme calcule la couleur de chaque pixel à partir des couleurs du sommet du triangle associé. Le calcul en question est une sorte de moyenne, où la couleur de chaque sommet est pondéré par un coefficient qui dépend de la distance avec le pixel. Plus le pixel est loin d'un sommet, plus ce 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, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge soit par l'étape de rastérisation, soit par les ''pixel shaders''.
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. A 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.
[[File:Phong shading.svg|centre|vignette|Interpolation des normales dans l'éclairage de Phong.]]
La différence entre les trois algorithmes se voit assez facilement à lécran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres.
{|
|-
|[[File:Flatshading01.png|vignette|upright=1|Flat shading]]
|[[File:Gouraudshading01.png|vignette|upright=1|Gouraud Shading]]
|[[File:Phongshading01.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting cube.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting cube.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting cube.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]]
|-
|[[File:Per face lighting example.png|vignette|upright=1|Flat shading]]
|[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]]
|[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]]
|}
===Le bump-mapping et autres approximations de l'éclairage par pixel===
Les techniques dites de '''''bump-mapping''''' visent à ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Les reliefs et autres détails ne sont pas présents dans la géométrie, mais sont ajoutés lors de l'étape d'éclairage. Ils sont mémorisés dans une texture appelée la ''bump-map'', qui est appliquée au-dessus que la texture normale.
[[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]]
La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Plus haut, nous avions vu que l'éclairage de Phong demande de calculer les lumières pour chaque pixel. Le ''normal-mapping'' consiste à précalculer les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait un calcul d'éclairage de type Phong avec. Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet.
[[File:NormalMaps.png|centre|vignette|upright=2|Normal Maps.]]
[[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]]
==Les ''shaders''==
Le rendu graphique a beaucoup évolué avec le temps. Lestrict minimum pour rendre une image texturée est de gérer la géométrie, la rastérisation, les textures, et un z-buffer. Mais avec le temps, des fonctionnalités de plus en plus complexes sont apparues. Et la plus importante est celle des '''''shaders'''''. Il s'agit de programmes informatiques que l'API fait exécuter par la carte graphique. Ils permettent de programmer des effets des effets graphiques, qui sont ensuite exécutés par la carte 3D.
===L'utilisation des ''shaders'' pour les algorithmes d'éclairage===
Les shaders servaient initialement à coder des algorithmes d'éclairage. Tout les algorithmes vus plus haut peuvent être programmés avec un shader adéquat, que ce soit du ''vertex lighting'', de l'éclairage par pixel, l'éclairage de type Gouraud, Phong et bien d'autres. Il existe plusieurs types de shaders, mais les deux principaux sont les vertex shaders et les pixel shaders. Pour simplifier grandement, les pixels shaders s'occupent de l'éclairage par pixel, alors que les vertex shaders s'occupent de l'éclairage par sommet. Un vertex shader permet d'implémenter un éclairage de type plat ou de type Gouraud, alors qu'il faut un pixel shader pour implémenter un éclairage de type Phong. Les techniques de bump-mapping et de normal-mapping peuvent aussi s'implémenter avec des pixel shaders.
L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Sans shaders, les algorithmes d'éclairage doivent être intégrés dans la carte graphique pour fonctionner, avec un circuit distinct pour chaque algorithme. Si la carte graphique ne gère pas un algorithme d'éclairage, 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. Et le tout avec des performances plus que convenables.
===Les autres utilisations des ''shaders''===
Cependant, l'usage des shaders dépasse le cadre des algorithmes d'éclairage. Les shaders modernes prennent en charge des fonctionnalités qui n'étaient autrefois pas programmables.
Par exemple, les calculs géométriques sont pris en charge par les vertex shaders. 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.
Les anciennes cartes graphiques faisaient les calculs géométriques dans un circuit fixe, non-programmable, appelé le circuit de ''Transform & Lightning''. Il s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). La première carte graphique à avoir intégré un circuit de T&L était la Geforce 256. A la même époque, les consoles de jeu n'avaient pas de circuit de T&L et les calculs géométriques étaient réalisés sur le CPU. La toute première carte graphique à avoir intégré des shaders était la Geforce 3, sur laquelle les vertex shaders sont apparus pour remplacer l'unité de T&L.Par contre, les pixel shaders sont apparus après. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs.
{{NavChapitre | book=Les cartes graphiques
| prev=Les cartes d'affichage des anciens PC
| prevText=Les cartes d'affichage des anciens PC
| next=Le rendu d'une scène 3D : l'API graphique
| nextText=Le rendu d'une scène 3D : l'API graphique
}}{{autocat}}
plxc7cuq5oakejcqexuf052sb992vtp
Les cartes graphiques/La microarchitecture des processeurs de shaders
0
81538
763318
763309
2026-04-09T14:33:08Z
Mewtow
31375
/* Les hybrides SIMD/VLIW et les instructions à co-issue */
763318
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, en dehors d'un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
Un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
==L'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité de ''Fetch'' qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
Les processeurs de shader sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de shaders en données. Et ces derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
==Les processeurs de shaders VLIW==
Une autre forme d'émission multiple est l'usage d'un jeu d'instruction VLIW et/ou d'instructions en ''co-issue'', abordés dans les chapitres précédents. Pour rappel, un processeur VLIW regroupe plusieurs opérations en une seule instruction machine. L'instruction encode les calculs à faire en parallèle, en ''co-issue''. Elle précise les registres, l'opcode de l'instruction, et tout ce qu'il faut pour faire les calculs, et attribue implicitement une unité de calcul à chaque opération.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard''. Mieux valait utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
===Les architectures VLIW pures, sans unité SIMD===
Un processeur VLIW contient un grand nombre d'unités de calcul. En théorie, il incorpore plusieurs unités de calcul scalaires séparés et n'a pas d'unité de calcul SIMD. Il y a cependant quelques exceptions, mais nous les verrons plus tard. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
La gestion des instructions en ''co-issue'' peut aussi utiliser les techniques des processeurs VLIW. Pour rappel, l'exemple typique d'instruction en ''co-issue'' regroupe une opération SIMD avec une opération scalaire, pour profiter de la présence d'une ALU scalaire séparée de l'ALU SIMD. Les cartes graphiques modernes gérent la ''co-issue'' aisni, avec la possibilité de ''co-issue'' une opération scalaire et une opération vectorielle. L'opération scalaire est souvent une opération entière, éventuellement flottante. Les processeurs de shaders qui supportent de telles instructions sont un hybride entre VLIW et SIMD.
Sur les anciennes cartes graphiques disposant d'une unité SIMD, la ''co-issue'' fonctionnait différemment, car les unités de calcul entières et flottantes n'étaient pas présentes. Seule l'unité de calcul transcendantale était présente, car très utile. Aussi, une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Le cas le plus simple est le processeur de vertices de la Geforce 3, avec une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple est le processeur de ''vertex shader'' de la Geforce 6800, illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2.0|Processeur de shader (vertex shader) d'une GeForce 6800. On voit clairement que celui-ci contient, outre les traditionnelles unités de calcul et registres temporaires, un "cache" d'instructions, des registres d'entrée et de sortie, ainsi que des registres de constante.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD le vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
2r57t15qzm2poz38sqjhvzr2dpvzl3s
763322
763318
2026-04-09T15:02:43Z
Mewtow
31375
/* Les processeurs de shaders VLIW */ Déplacement dans un chapitre séparé
763322
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, en dehors d'un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
Un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
==L'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité de ''Fetch'' qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
Les processeurs de shader sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de shaders en données. Et ces derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shaders
| prevText=Les processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ssmercnn0o9h6vh6wv1oweu2b4f206b
763324
763322
2026-04-09T15:03:21Z
Mewtow
31375
/* L'Operand Collector et les caches de register reuse */
763324
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, faisons un rappel rapide. Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, en dehors d'un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
Un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
==L'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité de ''Fetch'' qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
Les processeurs de shader sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de shaders en données. Et ces derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shader VLIW et DirectX 9
| prevText=Les processeurs de shader VLIW et DirectX 9
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
4q8iwd4i58r36uixgygnr1zjuxs4jt4
763328
763324
2026-04-09T15:06:20Z
Mewtow
31375
763328
wikitext
text/x-wiki
La conception interne (aussi appelée microarchitecture) des processeurs de ''shaders'' possède quelques idiosyncrasies. Mais avant d'expliquer lesquelles, nous devons prévenir d'une chose importante : dans ce chapitre, nous ne parlerons que des GPU de l'époque DirectX 10 et après, pas des GPU de l'époque DirectX 9. La raison est que leur jeu d'instruction a franchement évolué, avec le passage d'architectures VLIW à des architectures SIMD. Et cela a eu des conséquences assez profondes sur le jeu d'instruction et leur microarchitecture. Nous n'allons parler des GPU de type SIMD dans ce chapitre. Un chapitre dédié sera consacré aux GPU de type VLIW.
Pour rappel, un processeur de shader supporte plusieurs types d'instructions. Au minimum, il supporte des ''instructions SIMD''. Mais il peut aussi gérer des ''instructions scalaires'', à savoir qu'elles travaillent sur des entiers/flottants isolés, en dehors d'un vecteur SIMD. Typiquement, il y a plusieurs types d'instructions scalaires : les calculs entiers, les calculs flottants simples, les calculs flottants complexes dit transcendantaux (calculs trigonométriques, des exponentielles, des logarithmes, des racines carrées ou racines carrées inverse).
En clair, un processeur de shader doit savoir faire des calculs. Et il a des circuits dédiés pour ça, appelés des unités de calcul. Les unités de calcul sont souvent classées en deux types : les ALU et les FPU. Les premières font des calculs sur des opérandes entiers, alors que les secondes font des calculs flottants. Au passage, le terme ALU signifie ''Arithmetic and Logic Unit'', alors que FPU signifie ''Floating Point Unit'', les deux termes étant assez parlants. Mais un GPU utilise non seulement des ALU et des FPU, mais aussi des regroupements de plusieurs ALU/FPU. Voyons cela en détail.
==Les unités de calcul d'un processeur de shader SIMD==
Un processeur de shader contient un grand nombre d'unités de calcul très différentes. Le cœur est une unité de calcul SIMD, qui se charge des instructions SIMD. A cela, il faut souvent rajouter une ALU ou une FPU, parfois des unités de calcul flottantes spécialisées pour les opérations complexes, comme des opérations trigonométriques ou transcendantales.
===Les unités de calcul SIMD===
[[File:SIMD2.svg|vignette|Une unité de calcul SIMD.]]
Un processeur de shader incorpore une unité de calcul SIMD, qui effectue plusieurs calculs en parallèle. Elle regroupe plusieurs ALU flottantes regroupées ensemble et avec quelques circuits pour gérer les débordements et d'autres situations. En théorie, une unité de calcul SIMD regroupe autant d'unité de calcul qu'il y a d'entiers/flottants dans un vecteur. Par exemple, pour additionner deux vecteurs contenant chacun 16 flottants, il faut utilise 16 additionneurs flottants. Ce qui fait qu'une opération sur un vecteur est traité en une seule fois, en un cycle d'horloge. Une contrainte importante est que toutes les sous-ALU effectuent la même opération : ce ne sont pas des ALU séparées qu'on peut commander indépendamment, mais une seule ALU regroupant des circuits de calcul distinct.
Le cout en circuit est d'autant plus grand que les vecteurs sont longs et le cout est approximativement proportionnel à la taille des vecteurs. Entre des vecteurs de 128 et 256 bits, l'unité de calcul utilisera globalement deux fois plus de circuits avec 256 bits qu'avec 128. Même chose pour les registres, mais c'est là un cout commun à toutes les architectures.
Il y a quelques unités de calcul SIMD où le calcul se fait en deux fois, car on n'a que la moitié des unités de calcul. Par exemple, pour un vecteur de 32 flottants, on peut utiliser 16 unités de calcul, mais le temps de calcul se fait en deux cycles d'horloge. Les opérations sur les vecteurs sont donc faites en deux fois : une première passe pour les 16 premiers éléments, une seconde passe pour les 16 restants. L'implémentation demande cependant qu'une instruction de calcul soit décodée en deux micro-opérations. Par exemple, une instruction SIMD sur des vecteurs de 32 éléments est exécutée par deux micro-instructions travaillant sur des vecteurs de 16 éléments. On économise ainsi pas mal de circuits, mais cela se fait au détriment de la performance globale.
L'avantage est que cela se marie bien avec l'abandon des opérations pour lesquelles masques dont tous les bits sont à 0. Par exemple, prenons une instruction travaillant sur 16 flottants, exécutée en deux fois sur 8 flottants. Si le masque dit que les 8 premières opérations ne sont pas à exécuter, alors l'ALU ne fera que le calcul des 8 derniers flottants. Pour cela, le décodeur doit lire le registre de masque lors du décodage pour éliminer une micro-instruction si besoin, voire les deux si le masque est coopératif.
===Plusieurs unités SIMD, liées au format des données===
Il faut préciser qu'il y a une séparation entre unités SIMD flottantes simple et double précision. Pour le dire plus clairement, il y a des unités SIMD pour les flottants 32 bits, d'autres pour les flottants 64 bits, et même d'autres pour les flottants 16 bits. Les flottants 64 bits sont utilisés dans les applications GPGPU, les flottants 16 et 32 bits le sont dans le rendu 3D, et les flottants 16 bits pour tout ce qui lié à l'IA. Malheureusement, on doit utiliser des ALU flottantes séparées pour chaque taille, le format des flottants n'aidant pas.
Depuis plus d'une décennie, les cartes graphiques ont des unités SIMD à la fois pour les calculs entiers et flottants. Elles sont censées être séparées. Pour NVIDIA, avant l'architecture Turing, les unités SIMD entières et flottantes sont décrites comme séparées dans leurs ''white papers'', avec des unités INT32 et FLOAT32 séparées, combinées à d'autres unités de calcul. L'architecture Volta a notamment des unités INT32, FLOAT32 et FLOAT64 séparées. A partir de l'architecture Ampere, il semblerait que les unités SIMD soient devenues capables de faire à la fois des calculs flottants et entiers, pour la moitié d'entre elles. Mais il se pourrait simplement qu'elles soient physiquement séparées, mais reliées aux reste du processeur de manière à ne pas être utilisables en même temps.
Il est cependant possible que sur d'anciennes architectures, les unités entières et flottantes partagent des circuits, notamment pour ce qui est de la multiplication. En effet, une unité de calcul flottante contient des circuits pour faire des calculs entiers pour additionner/multiplier les mantisses et les exposants. Il est possible d'utiliser ce circuit Un exemple est le cas de l'architecture GT200 d'NVIDIA, sur laquelle les "pseudo-ALU" entières SIMD étaient limitées à des multiplications d'opérandes 24 bits, ce qui correspond à la taille d'une mantisse d'un flottant 32 bits. Le design exact des ALU n'est pas connu.
===Les unités de calcul scalaires===
Les GPU modernes incorporent une '''unité de calcul entière scalaire''', séparée de l'unité de calcul SIMD. Elle gère des calculs scalaires, à savoir qu'elle ne travaille pas sur des vecteurs. Elle gère divers calculs, comme des additions, soustractions, comparaisons, opérations bit à bit, etc. Elle exécute les instructions de calcul entière sur des nombres entiers isolés, de plus en plus fréquentes dans les shaders.
Elle est parfois accompagnée d'une unité de calcul pour les branchements. Par branchements, on veut parler des vrais branchements similaires à ceux des CPU, qui effectuent des tests sur des entiers et effectuent des branchements conditionnels. Ils n'ont rien à voir avec les instructions à prédicat qui elles sont spécifiques à l'unité de calcul vectorielles. Ce sont des instructions séparées, totalement distinctes
Les processeurs de shaders incorporent aussi une '''unité de calcul flottante scalaire''', utilisée pour faire des calculs sur des flottants isolés. L'unité de calcul gère généralement des calculs simples, comme les additions, soustractions, multiplications et divisions. Il s'agit typiquement d'une unité de calcul spécialisée dans l’opération ''Multiply-And-Add'' (une multiplication suivie d'une addition, opération très courante en 3D, notamment dans le calcul de produits scalaires), qui ne gère pas la division.
L'unité de calcul flottante est souvent accompagnée d'une unité de calcul spécialisée qui gère les calculs transcendantaux, avec une gestion des calculs trigonométriques, de produits scalaires ou d'autres opérations. Elle porte le nom d''''unité de calcul spéciale''' (''Special Function Unit''), ou encore d'unité de calcul transcendantale, et elle a d'autres appellations. Elle est composée de circuits de calculs accompagnés par une table contenant des constantes nécessaires pour faire les calculs.
Rappelons que les registres SIMD et les registres scalaires sont séparés et ne sont pas adressés par les mêmes instructions. Les registres scalaires sont placés dans un banc de registre physiquement séparé du banc de registres SIMD. Le banc de registre scalaire est relié à sa propre ALU scalaire, il y a vraiment une séparation physique entre registres scalaires et SIMD. Il existe cependant un système d'interconnexion qui permet d'envoyer un scalaire aux unités SIMD, ce qui est utile pour les opérations de produits scalaire ou autres.
==L'intérieur d'un processeur de shader==
: Cette section sera surtout des rappels, pour ceux qui ont déjà lu un cours d'architecture des ordinateurs, et connait la notion de pipeline.
En plus des unités de calcul, un processeur contient d'autres circuits, et un processeur de shader ne fait pas exception. Un processeur est composé de quatre circuits principaux :
* les unités de calcul, qui font des calculs et d'autres opérations ;
* les registres pour mémoriser les opérandes des calculs et leurs résultats ;
* une unité mémoire pour échanger des données entre VRAM et registres ;
* une unité de contrôle qui exécute les instructions.
Les unités de calcul, les registres et l'unité mémoire sont souvent regroupés sous le terme de '''chemin de données'''. Le terme dit clairement que c'est la partie du processeur qui gère les données, les manipule, fait des calculs, etc. A côté du chemin de données, il y a une unité de contrôle, qui commande ce chemin de données pour qu'il fasse les instructions demandées. L'unité de contrôle lit les instructions depuis la mémoire vidéo, et configure le chemin de données, pour qu'il exécute l'instruction demandée. L'unité de contrôle sera détaillée dans la section suivante.
===Le chemin de données d'un processeur de shader===
Un processeur de shader contient au minimum une unité SIMD, et une unité scalaire. Il est possible d'utiliser les deux en même temps, grâce aux instructions à ''co-issue''. Elles sont surtout utiles pour exécuter des instructions scalaires en parallèle d'instruction SIMD, mais guère plus. Notez que j'ai dit : "au minimum une unité SIMD et une unité scalaire", car les processeurs de shaders modernes dupliquent les unités de calcul, pour des raisons qu'on expliquera dans la suite du chapitre. Il n'est pas rare qu'un processeur de shader dispose d'une dizaine d'unités SIMD et de 2 ou 3 unités scalaires.
Nous avons vu les registres dans le chapitre précédent, aussi je ne vais pas revenir dessus. Je vais juste préciser que les registres sont regroupés dans des '''bancs de registres'''. Ce sont de petites mémoires dont chaque adresse contient un registre. En général, il y a un banc de registre pour les scalaires et un autre pour les vecteurs SIMD. Les deux sont séparés car ils ne sont pas utilisés par les mêmes instructions. Et c'est plus pratique à implémenter.
L''''unité d'accès mémoire''' s'occupe des lectures et écriture en général, et elle prend en charge les accès aux textures, le filtrage de textures et tout un tas de fonctionnalités liées aux textures. La seule différence entre un accès aux textures et une lecture/écriture en mémoire est que les circuits de filtrage de texture sont contournés dans une lecture/écriture normale. Dans ce qui suit, nous allons l'appeler l'unité de texture par souci de simplification.
===L'unité de contrôle d'un processeur de shader===
L'unité de contrôle d'un GPU a quelques petites différences avec celles d'un CPU moderne. Les unités de contrôle des GPU n'utilisent pas les optimisations des CPU modernes, tant utiles pour du calcul séquentiel : pas d’exécution dans le désordre, de renommage de registres, et autres techniques avancées. A la place, elles utilisent des techniques alternatives qu'on décrira pdans la suite du chapitre, qui sont peu gourmandes en transistors. En conséquence, les unités de contrôle sont très simples, prennent peu de place, utilisent peu de transistors. La majeure partie du processeur est dédié aux unités de calcul et aux registres.
[[File:Cpu-gpu.svg|centre|vignette|upright=2.0|Comparaison entre l'architecture d'un processeur généraliste et d'un processeur de shaders.]]
Un processeur de shader a une unité de contrôle assez classique, composée de plusieurs circuits.
* une unité de ''Fetch'' qui calcule l'adresse de la prochaine instruction ;
* un cache d'instruction dans lequel on récupère la prochaine instruction, en présentant son adresse ;
* une unité de décodage d'instruction, qui traduit l'instruction en signaux de commande à destination de l'unité de calcul et des registres ;
* une unité d’''issue'', aussi appelée le ''scoreboard'', qu'on détaillera dans ce qui suit.
Le tout est illustré ci-dessous, avec le chemin de données. Vous remarquerez que dans le chemin de données, il y a aussi une unité pour enregistres les résultats dans les registres, qui effectue pas mal de traitements importants qu'on ne peut pas détailler ici. Vous remarquerez aussi que l'unité qui calcule l'adresse de la prochaine instruction est un peu complexe. Mais laissons cela de côté pour le moment.
[[File:Vortex microarchitecture.png|centre|vignette|upright=2.5|Exemple de microarchitecture d'un processeur de shader.]]
===Le pipeline d'un processeur de shader===
Un point important est que les processeurs de shaders utilisent la technique du '''pipeline'''. Les unités vues précédemment fonctionnent indépendamment des autres. Elles n'attendent pas que l'instruction soit terminée pour commencer à traiter la suivante. Concrètement, la première unité traite l'instruction courante, pendant que la suivante traite l'instruction précédente, et ainsi de suite :
* la première unité calcule l'adresse de l'instruction numéro N;
* le cache d'instruction lit l'instruction numéro N-1 ;
* l'unité de décodage décode l'instruction numéro N-2 ;
* le ''scoreboard'' analyse l'instruction numéro N-3 ;
* les unités de calcul exécutent l'instruction numéro N-4 ;
* l'unité d'enregistrement écrit le résultat de l'instruction numéro N-5 dans les registres.
Faire ainsi maximise les performances, car cela permet d'exécuter plusieurs instructions en même temps, à des étapes différentes. Le débit d'instruction est maximisé, les shaders s'exécutent plus vite. Le nombre d'étapes exact dépend du processeur. Il arrive que des processeurs fusionnent des étapes en un seul circuit. Par exemple, l'unité de décodage et le ''scoreboard'' peuvent être regroupés dans une seule étape, de même que l'accès aux registres et le calcul dans l'ALU. D'autres processeurs scindent certaines étapes en plusieurs sous-circuits séparés. Par exemple, il peuvent scinder leurs ALU en plusieurs sous-ALU, chacune exécutant un morceau de calcul.
Et cela permet d'expliquer pourquoi un processeur de ''shaders'' SIMD contient beaucoup d'unités de calcul, identiques, ou non. Les instructions prennent plusieurs cycles d'horloge à s’exécuter, ce qui fait qu'une instruction occupe une unité de calcul pendant 2 à 20 cycles, rarement plus. Cependant, le processeur peut démarrer une nouvelle instruction par cycle d'horloge. Et cela permet malgré tout de démarrer une nouvelle instruction dans une unité de calcul libre. Pendant qu'une instruction en est à son second ou troisième cycle dans une ALU, il est possible de démarrer une nouvelle instruction dans une ALU inoccupée, sans voir recours à la ''co-issue''. s'il y en a une.
Par exemple, reprenons l'exemple de l'unité de ''vertex shader'' de la Geforce 6800, mentionné au-dessus. Elle dispose d'une unité de calcul SIMD MAD, et d'une unité de texture, ainsi que d'une unité de calcul scalaire transcendantale. Il en en théorie possible de faire en même temps : un calcul dans l'ALU SIMD, une lecture de texture dans l'unité mémoire, un calcul trigonométrique dans l'unité transcendantale. Il suffit de lancer la lecture de texture à un cycle, l’instruction MAD au cycle suivant, et l'instruction spéciale deux cycles après.
===Le ''scoreboard'' d'un processeur de shader===
Exécuter plusieurs instructions en même temps pose un problème quand deux instructions consécutives sont dépendantes l'une de l'autre. Un cas classique est quand une instruction a besoin du résultat de la précédente. Dans ce cas, l'instruction accède aux registres alors que la première n'a pas encore écrit dedans, ce qui pose un problème. Il s'agit d'une dépendance dite RAW (''Read After Write'') typique, que la carte graphique doit gérer automatiquement. La seconde instruction ne doit pas démarrer tant que la précédente n'a pas enregistré son résultat dans les registres.
Le ''scoreboard'' gère ce genre de problèmes. Il détecte les dépendances entre instructions et les gère sans intervention extérieure. Pour cela, il vérifie si les opérandes de l'instruction sont en cours de calcul. Pour cela, il regarde quels registres l'instruction lit/écrit, et vérifie s'ils sont en cours d'utilisation. Pour savoir quels registres sont en cours d'utilisation, rien de plus simple : quand il démarre une instruction, le ''scoreboard'' marque ses registres comme en cours d'utilisation.
Le ''scoreboard'' se contente de bloquer l'instruction tant qu'elle ne peut pas s'exécuter. Et les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Cependant, les processeurs de shaders disposent de plusieurs optimisations concernant ce genre de situations. Voyons cela en détail.
Et au-delà de ça, le ''scoreboard'' bloque l'exécution d'une instruction si les conditions ne sont pas remplies. Notamment, il vérifie qu'il y a une unité de calcul libre pour exécuter l'instruction. Si ce n'est pas le cas, l'instruction est bloquée. Au-delà de ça, il existe d'autres dépendances liées au fait que deux instructions utilisent les mêmes registres. Mais avec 4096 registres par ''shader'', elles sont plus rares, ce qui fait qu'on les laisse volontairement de côté.
===Exemple et résumé final===
Pour finir, nous allons voir un exemple final, celui des GPU Radeon X1000 series, de microarchitecture R500, Terascale. Leur microarchitecture est résumée dans le schéma ci-dessous. La portion gauche du schéma montre plusieurs choses. Le GPU contient un processeur de commande, appelé l'''ultra threaded dispatcher'' sur ces GPU. Il alimente plusieurs processeurs de shaders, ici appelés des ''compute unit'' (CU).
De nombreux circuits sont partagés entre plusieurs processeurs. Par exemple, ils partagent un cache L2, dans lequel ils viennent récupérer les données nécessaires. Le GPU contient aussi des contrôleurs mémoire, qui lisent ou écrivent des données en mémoire vidéo. Les contrôleurs mémoire servent surtout d'interface entre la mémoire vidéo et le cache L2, mais ils peuvent aussi envoyer des données lue aux processeurs de shaders.
Le focus de droite montre ce qu'il y a dans un processeur de shader. Déjà, l'unité de contrôle est en haut et est nommée ''Fetch, Decode, Schedule'' : ''Schedule'' est un synonyme de ''Issue''. L'unité mémoire est appelée la ''Load Store Unit'' (LSU), elle communique avec la mémoire vidéo et les registres. A côté, on trouve une unité de calcul SIMD et une unité de calculs scalaire. Pour ce qui est des mémoires, elle montre qu'il y a une petite mémoire locale généraliste, complétée avec deux bancs de registres : un pour les données scalaires, un pour les vecteurs SIMD.
* Les registres pour les scalaires sont appelés les ''Scalar General Purpose Registers'' (GPR).
* Les registres pour les vecteurs SIMD sont appelés les ''Vector General Purpose Registers'' (VGPR).
* La mémoire locale généraliste, appelée la mémoire partagée (LDS).
Niveau interconnexions, les flèches montrent plusieurs choses. Premièrement, l'unité mémoire est reliée aux bancs de registres, ainsi qu'à la mémoire locale. Les accès à la mémoire locale passent par l'unité mémoire, qui sert d'intermédiaire obligatoire. L'unité SIMD est connectée aux registres SIMD, l'unité scalaire est reliée au banc de registres scalaire. Rien d'étonnant. L'unité SIMD peut aussi lire des scalaires pour certaines instructions SIMD verticales, ce qui fait qu'elle est aussi connecté au banc de registres scalaires.
[[File:MIAOW GPU diagram.png|centre|vignette|upright=2|Microarchitecture d'un GPU, avec un focus sur un processeur de shader.]]
==Le ''multithreading'' matériel des processeurs de shaders==
L'unité d'''issue'' détecte les dépendances de données, et bloque les instructions si elles ne doivent pas s'exécuter. L'inconvénient est que, quand une instruction est bloquée, les instructions suivantes sont aussi bloquées dans l'étage où elles sont. Rien ne progresse dans le pipeline tant que l'instruction fautive est bloquée. Heureusement, les GPU et les CPU disposent de techniques pour surmonter ce blocage, afin de continuer à exécuter des instructions.
Les CPU disposent de techniques d'exécution dans le désordre, de renommage de registre, et bien d'autres. Mais leur implémentation demande un budget en transistor conséquent, que les GPU ne peuvent pas se permettre. A la place, ils utilisent une technique appelée le '''''multithreading'' matériel''', qui vient du monde des CPU. Vous connaissez sans doute l'''hyperthreading'' d'Intel ? C'est une version basique du ''multithreading'' matériel.
L'idée est d'exécuter plusieurs programmes en même temps sur le même processeur, le processeur commutant de l'un à l'autre suivant les besoins. Par exemple, si un programme est bloqué par un accès mémoire, d'autres programmes exécutent des calculs dans l'unité de calcul en parallèle de l'accès mémoire.
Pour un GPU, les programmes en question sont des instances de shader qui travaillent sur des données différentes. Ces instances de shader portent les noms de ''warp'' dans la terminologie NVIDIA, mais on peut aussi parler de ''threads'' pour utiliser la même terminologie que pour les CPUs. Un processeur de ''shader'' commute donc régulièrement d'un ''warp'' à l'autre, suivant les besoins. Dans ce qui va suivre, nous allons voir dans quelles situations un processeur de ''shader'' change de ''thread''/''warp'' en cours d'exécution. Suivant le GPU, les situations ne sont pas les mêmes.
Il existe trois techniques de ''multithreading'' matériel : le ''Fine Grained Multithreading'', le ''Coarse Grained Multithreading'' et le ''Simultaneous MultiThreading''. Dans ce qui suit, nous utiliserons l'abréviation FGMT pour parler du ''Fine Grained Multithreading'', de CGMT pour parler du ''Coarse Grained Multithreading'' et de SMT pour le ''Simultaneous MultiThreading''. Les GPU ont d'abord implémenté le CGMT dans la période DirectX 9, puis sont passé au FGMT, avant de passer au SMT sur les générations récentes. Aussi, nous allons les voir dans l'ordre.
===Le ''Coarse Grained Multithreading'' de l'époque DirectX 9===
Les processeurs de shader sont connectés à une mémoire vidéo très lente, avec un temps d'accès élevé, qui se rattrape avec un débit binaire important. La conséquence est qu'un accès à une texture, c'est long : si celle-ci est lue depuis la mémoire vidéo, le temps d'attente est d'une bonne centaine de cycles d'horloges. Pour limiter la casse, les unités de texture incorporent un cache de texture, mais cela ne suffit pas toujours à alimenter les processeurs de shaders en données. Et ces derniers ne peuvent pas recourir à des techniques avancées communes sur les CPU, comme l’exécution dans le désordre : le cout en circuit serait trop important.
[[File:Coarse Grained Multithreading.png|thumb|Coarse Grained Multithreading.]]
Fort heureusement, les processeurs de shaders utilisent le ''multithreading'' matériel pour masquer la latence des accès mémoire. L'idée est que si un ''thread'' démarre un accès mémoire, il est mis en pause pendant l'accès mémoire, et laisse sa place à un autre ''thread''. Ainsi, pendant qu'un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' utilise les unités de calcul en parallèle. Cela permet de masquer la latence des accès mémoire.
: Notons qu'avec cette technique, les lectures mettent en pause le ''thread'' qui les exécute. On parle alors de '''lectures bloquantes'''. Nous verrons que les processeurs de shader plus récents exécutent des lectures non-bloquantes, mais ce sera pour la suite.
La technique ne donne de bons résultats que si les accès mémoire sont peu fréquents, ou que le nombre de ''threads'' est élevé. Plus les accès mémoire sont fréquents, plus il faut un nombre de ''threads'' important pour masquer la latence. A l'époque, il était rare que les ''vertex shader'' accèdent à des textures, alors que les pixels shaders ne faisaient que ça. Ls processeurs de shaders de la Geforce 6 géraient au maximum 4 ''threads'' simultanés, alors que les processeurs de pixel shaders en géraient facilement plus d'une centaine. Les cartes concurrentes d'ATI supportaient 128 ''threads'' maximum, par processeur de pixel shader.
L'implémentation de cette technique est assez simple, au premier abord. Déjà, il y a un ''Program Counter'' par ''thread''. À chaque cycle, un multiplexeur choisit le ''Program Counter'' - le thread - qui a la chance de charger ses instructions. Le choix du ''program counter'' sélectionné est le fait de l''''unité d'ordonnancement'''. L'unité d'ordonnancement sait quels ''threads'' sont en cours d'exécution et lesquels sont en pause. Pour cela, elle intègre une petite mémoire qui mémorise l'état de chaque ''thread''.
[[File:Architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Architecture d'un processeur multithreadé]]
La technique impose cependant que les registres soient dupliqués, pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. Et il y a la même chose avec d'autres structures matérielles, comme les files de lecture/écriture de l'unité d'accès mémoire.
[[File:Aperçu de l'architecture d'un processeur multithreadé.png|centre|vignette|upright=2|Aperçu de l'architecture d'un processeur multithreadé]]
: La technique générale porte le nom de ''Coarse Grained Multithreading''. C'est une forme de ''Multithreading'' où on change de programme quand un évènement bien précis a lieu. Les GPU en utilisaient une version où les évènements en question sont des accès mémoire. En théorie, la technique s'adapte pour d'autres opérations que les accès mémoire, tant que celles-ci prennent beaucoup de temps. Mais je ne saurais dire si les GPU l'appliquaient pour autre chose que les accès mémoire.
===Interlude propédeutique : le ''Fine Grained Multithreading''===
[[File:Fine Grained Multithreading.png|thumb|Fine Grained Multithreading]]
Peu avant les années 2010, les processeurs de shaders ont subit quelques changements, afin d'augmenter leur performance. Notamment, le nombre d'étapes du pipeline a augmenté, histoire d’exécuter plus d'instructions simultanément. Et cela a commencé à poser quelques problèmes : les situations où deux instructions utilisent les mêmes registres a augmenté, les dépendances de données sont devenues un problème.
Et pour résoudre ces problèmes, les GPU ont basculé du CGMT vers une forme de ''multithreading'' qu'on ne trouve que sur les GPU, ou presque. Et pour la comprendre, nous allons devoir faire un léger détour par une forme de ''multithreading'' très similaire, utilisée sur les CPU. Il s'agit du ''Fine Grained Multithreading'' (FGMT).
Avec le FGMT, le processeur de shader change de ''thread'' à chaque cycle d'horloge. Le processeur fait donc une rotation, à chaque cycle, parmi les ''threads'' actifs. Les ''threads bloqués'' par un accès mémoire ne sont pas pris en compte. Par exemple, imaginons que le processeur gère 32 ''threads'' simultanés, mais que 8 d'entre eux soient en pause lors d'un accès mémoire. Dans ce cas, il changera de ''thread'' tous les 24 cycles, car il ne prend en compte que les ''threads'' non-bloqués.
Faire ainsi a de nombreux avantages, notamment pour ce qui est des dépendances entre instructions. En changeant de ''thread'' à chaque cycle, on "espace" les instructions d'un même ''thread''. Par exemple, si on a 8 ''threads'' qui s'exécutent en même temps, alors il y a 8 cycles d'horloges entre deux instructions d'un même ''thread''. La première instruction a alors eu tout le temps pour enregistrer son résultat dans les registres, avant même que la seconde instruction lise ses opérandes. Le processeur peut alors utiliser un ''scoreboard'' très limité, très simple, pour détecter les rares dépendances de données qui restent, voire peut se passer complétement de ''scoreboard'' !
===Le ''Multithreading'' commandé par le ''scoreboard'' des années 2005-2010===
[[File:Full multithreading.png|thumb|Full multithreading]]
Sur les GPU récents, le processeur de shader ne change pas de ''thread'' à chaque cycle. Il préfère exécuter plusieurs instructions consécutives d'un même ''thread'', dans des cycles d'horloge consécutifs. Il ne change de ''thread'' que quand une dépendance de donnée bloque une instruction. C'est donc le ''scoreboard'' qui commande le changement de ''thread'', là où le changement de ''thread'' était réalisé sans lui avec le CGMT ou le FGMT.
L'implémentation matérielle sépare le processeur de ''shader'' en deux sections séparées par une mémoire tampon qui met en attente les instructions. Les instructions sont chargées et décodées, puis sont placées dans une ou plusieurs '''files d'instruction'''. Le cas le plus simple à comprendre utilise une file d'instruction par ''thread''. A chaque cycle, l'unité d'émission vérifie plusieurs instructions, une par file d'instruction. Il choisit alors une instruction prête, puis l'envoie aux unités de calcul. L'instruction peut venir de n'importe quelle file d'instruction. Mais pour que cela fonctionne, il faut que les files d'instructions soient remplies, ce qui implique que le processeur doit avoir chargée des instructions en avance.
Plus haut, nous avions dit que l'unité de chargement mémorise plusieurs ''program counter'', un par ''thread'', et choisit à chaque cycle l'un d'entre eux pour charger une instruction. Le choix en question est synchronisé avec l'émission des instructions. Si un ''thread'' émet une instruction, ce même ''thread'' charge une instruction au même moment. Sauf si la file d'instruction du ''thread'' est déjà pleine, auquel cas un autre ''thread'' est choisi.
Précisons que le grand nombre de registres et de ''threads'' fait qu'un ''scoreboard'' classique, tel que décrit dans les cours d'architecture des ordinateurs, devient rapidement impraticable. Aussi, une implémentation alternative est utilisée, bien que les détails ne soient pas connus à ce jour. Quelques brevets donnent des détails d'implémentation, mais on ne sait pas s'ils sont à jour. Les curieux peuvent tenter de lire le brevet américain numéro #7,634,621, nommé ''"Register File Allocation"'', déposé par NVIDIA durant décembre 2009.
[[File:FGMT sur un processeur de shaders.png|centre|vignette|upright=2|FGMT sur un processeur de shaders]]
L'usage d'un ''scoreboard'' permet de nombreuses optimisations, qui portent notamment sur les lectures, qui interagissent avec le FGMT. L'optimisation en question s'appelle les '''lectures non-bloquantes'''. Elle fonctionne si la lecture est suivie par d'autres instructions qui n’accèdent pas à la mémoire, par exemple des instructions de calculs. L'idée est d'exécuter ces instructions en avance, pendant que la lecture récupère la donnée. C'est possible car la lecture utilise l'unité mémoire et le cache, mais laisse libre les unités de calcul et les registres. En clair, pendant qu'on lit une texture, on fait des calculs en parallèle dans les ALU.
Mais la technique n'est possible que si les instructions de calcul n'utilisent pas la donnée en cours de lecture. Heureusement, le ''scoreboard'' détecte si cela arrive en surveillant les registres. La lecture charge une donnée dans un registre, appelé le registre de destination (sous entendu, de destination de la lecture). Les instructions qui n'utilisent pas ce registre peuvent s'exécuter sans problèmes : elles sont indépendantes de la lecture. Mais dès qu'une instruction souhaite lire ou écrire dans ce registre, elle est bloquée par le ''scoreboard'' et le processeur change de ''thread''. Détecter les dépendances demande juste de mémoriser le registre de destination dans un registre temporaire, et de regarder si l'instruction à exécuter utilise ce registre.
Une différence avec le CGMT est le moment où un ''thread'' est bloqué par une lecture. Avec des lectures bloquantes, un ''thread'' est bloqué dès que la lecture est lancée. Et ce même si les instructions suivantes sont indépendantes de la lecture. Par contre, avec les lectures non-bloquantes, un ''thread'' n'est pas bloqué à ce moment-là. Il continue son exécution jusqu'à ce qu'une instruction accède à la donnée lue. Si la lecture s'est déjà terminée, alors la donnée est disponible et le ''thread'' continue de s'exécuter. Mais si la lecture est encore en cours, alors le processeur bloque l'instruction et change de ''thread''.
Une autre optimisation possible est l'usage de l''''émission multiple'''. Avec elle, le ''scoreboard'' peut émettre deux instructions lors du même cycle d'horloge, si elles utilisent des unités de calcul différentes. Les deux instructions doivent faire partie du même ''thread'', mais certains GPU acceptent qu'elles soient de deux ''threads'' différents, tout dépend du GPU. Par exemple, il est possible qu'une instruction d'un ''thread'' utilise l'unité de calcul SIMD, alors que l'autre lance une lecture de texture. Ou encore, les deux peuvent lancer des calculs SIMD, mais dans deux unités SIMD séparées.
===L'encodage explicite des dépendances sur les GPU post-2010===
Depuis environ 2010, les GPU n'utilisent plus de ''scoreboard'' proprement dit. A la place, les GPU modernes déportent la détection des dépendances de données à la compilation. L'idée est que chaque instruction contient quelques bits pour dire au processeur : tu peux lancer 1, 2, 3 instructions à la suite sans problème. La technique porte le nom d''''anticipation de dépendances explicite''' (''Explicit-Dependence lookahead''). Un exemple historique assez ancien est le processeur Tera MTA (''MultiThreaded Architecture''), qui utilisait cette technique.
Les GPU NVIDIA modernes utilisent plusieurs bits par instruction pour gérer les dépendances de données. Un premier mécanisme est utilisé pour bloquer l'émission d'une nouvelle instruction pendant x cycles. Il utilise un '''''stall counter''''', qui mémorise le nombre de cycles d'attente. Une instruction peut initialiser le ''stall counter'' avec une valeur de base, qui indique combien de cycles attendre. Le ''stall counter'' est décrémenté à chaque cycle d'horloge et une nouvelle instruction s'exécute seulement quand le compteur atteint 0. Il est utilisé quand une instruction productrice prenant X cycles est suivie par une instruction consommatrice, le ''stall counter'' est alors initialisé à la valeur X.
La méthode précédente ne fonctionne que pour les instructions dont le compilateur peut prédire la durée. Les accès mémoire et certains calculs complexes ne sont pas dans ce cas. Pour les gérer, les GPU modernes utilisent un autre mécanisme, basé sur des '''compteurs de dépendances'''. Il y en a entre 5 et 10 selon le GPU. Une instruction dite productrice réserve un de ces compteurs, l'incrémente quand elle démarre son exécution/est émise, le décrémente quand la dépendance est résolue. Les instructions consommatrices, qui utilisent le résultat de l'instruction productrice, attendent que le compteur tombe à zéro pour s'exécuter. Elles précisent quels compteurs regarder avec un '''masque de compteurs de dépendance''', encodé directement dans l'instruction elle-même.
Précisément, chaque instruction productrice se réserve deux compteurs, pour gérer les trois types de dépendances de données (RAW, WAR et WAW). Le premier compteur est décrémenté quand le résultat est écrit dans les registres, ce qui gère naturellement les dépendances RAW et WAW. Le second est décrémenté quand l'instruction a lu ses opérandes, ce qui gère les dépendances WAR. Les autres instructions regardent l'un ou l'autre des compteurs selon leur situation.
Il arrive que le switch de ''thread'' soit déclenché par des bits intégrés dans l'instruction. Par exemple, sur les GPU NVIDIA modernes, chaque instruction contient un bit '''''yield''''' qui indique qu'il faut changer de ''thread'' une fois l'instruction émise. En clair, il indique que l'instruction risque de durer longtemps, a des dépendances avec la mémoire ou autre chose qui fait qu'il est préférable de changer de ''thread''.
==Le banc de registres d'un processeur de ''shader''==
Les GPU disposent d'un grand nombre de registres. Les normes de DirectX et Open GL imposent que les shaders modernes gèrent au moins 4096 registres généraux par instance de shader, avec des registres spécialisés en plus. En soi, 4096 est énorme ! Mais au-delà de ces normes, le FGMT implique de dupliquer des registres par le nombre de ''threads hardware'' simultanés. Avec le FGMT, les registres devront être dupliqués pour que chaque ''thread'' ait ses propres registres rien qu'à lui. Sans cela, impossible de passer rapidement d'un ''thread'' à l'autre à chaque cycle.
Maintenant, faisons quelques calculs d'épiciers. Un processeur de ''shader'' peut exécuter entre 16 et 32 ''threads''/''warps'', ce qui multiplie le nombre de registres par 16/32. En multipliant par les 4096 registres nécessaires, cela fait 128 kilooctets de mémoire rien que pour les registres. Et c'est pour un seul cœur ! Si on multiplie par le nombre de cœurs, on trouve que les cartes graphiques modernes ayant plusieurs processeurs de ''shaders'' ont facilement entre 32 768 et 65 536 registres de 32 bits, ce qui est énorme ! Il y a plus de mémoire gaspillée dans les registres que dans le cache L1 ou L2 !
Et ce grand nombre de registres par cœur pose quelques problèmes. Les registres sont regroupés dans une petite mémoire SRAM, adressable, appelée le '''banc de registre'''. Et comme toutes les mémoires, plus ce banc de registres est grand, plus il est lent. En conséquence, lire un opérande dans les registres prend beaucoup de temps. Du moins, c'est le cas sans optimisations. Et les GPU implémentent de nombreuses parades pour limiter le nombre de registres réellement présents dans leur silicium.
===L'allocation dynamique/statique des registres par ''thread''===
Les GPU modernes n'implémentent pas le nombre maximal de registres demandés. Par exemple, si je prends les GPU AMD de type RDNA 4, il peuvent gérer 16 ''threads'' hardware simultanés, chacun ayant accès à 256 registres, registres faisant 128 octets chacun. On s'attend à avoir un banc de registre de 16 * 256 * 128 octets, soit 512 Kilo-octets. En somme, un banc de registre de la taille du cache L1. Sauf que les GPUs en question intègrent moins de registres que prévu ! Ils ont précisément une taille de 192 kilo-octets, soit 96 registres pour chacun des 16 ''threads''.
En effet, 256 registres est un nombre maximal, que la plupart des ''shaders'' n'utilise pas totalement. La plupart des ''shaders'' utilise entre 64 et 128 registres, rarement moins, rarement plus. Aussi, le GPU partitionne le banc de registres à la demande entre les ''threads'', en leur donnant seulement une partie des registres. Le partitionnement peut être pseudo-statique, à savoir que le banc de registre est découpé en parts égales pour chaque ''thread'', ou dynamique avec un nombre de registre variant d'un ''thread'' à l'autre, selon les besoins.
Prenons l'exemple d'un '''partitionnement pseudo-statique''', avec l'exemple des GPU AMD RDNA 1 et 2. Leur banc de registre fait 1024 registres, de 128 octets chacun, soit 128 KB au total. Le GPU gère 16 ''threads'' simultanés maximum. Avec un seul ''thread'' d'exécuté, le ''thread'' unique peut utiliser les 1024 registres du banc de registre pour lui tout seul. Avec deux ''threads'', chacun aura droit à 512 registres, soit la moitié du cas précédent. Avec 16 ''thread'' simultanés, chaque ''thread'' a accès à 64 registres, pas plus. Et ainsi de suite : le nombre de registre par ''thread'' est égal à la taille du banc de registre divisée par le nombre de ''threads''.
A l'heure où j'écris ces lignes, courant 2025, les GPU Intel se contentent d'un partitionnement statique très limité. Avant les GPU d'architecture Battlemage, il n'y avait pas de partitionnement du banc de registre, tous les ''threads'' avaient 128 registre à leur disposition. Les GPU Battlemage et ses successeurs ont introduit un partitionnement limité, avec deux modes : un mode sans partitionnement où tous les ''threads'' ont accès à 128 registres, un mode avec partitionnement qui divise le nombre de ''thread'' par deux et leur donne chacun 256 registres. Pas de possibilité de diviser plus le nombre de ''threads''.
Les GPU AMD et NVIDIA sont eux plus compétents niveau partitionnement statique. Par exemple, les GPU RDNA 4 supportent les partitionnements suivants :
* 16 ''threads'' avec 96 registres chacun ;
* 12 ''threads'' avec 120 registres chacun ;
* 10 ''threads'' avec 144 registres chacun ;
* 9 ''threads'' avec 168 registres chacun ;
* 8 ''threads'' avec 192 registres chacun ;
* 7 ''threads'' avec 216 registres chacun ;
* 6 ''threads'' avec 240 registres chacun ;
* 5 ''threads'' avec 256 registres chacun.
Le partitionnement pseudo-statique est simple à implémenter, il ne demande pas beaucoup de circuits pour fonctionner. Il est rapide et a de bonnes performances pour le rendu graphique en rastérisation. La raison est qu'en rastérisation, les différents ''threads'' sont souvent des copies/instances d'un même ''shader'' qui travaillent sur des données différentes. Leur donner le même nombre de registres colle bien avec cet état de fait.
Cependant, si les différents ''threads'' sont des ''shaders'' différents, les choses ne sont pas optimales. Un ''shader'' utilisera plus de registres que l'autre, leur donner le même nombre de registres n'est pas optimal. Par exemple, imaginons que l'on a deux shaders, nommés shaders 1 et 2, aux besoins différents, l'un étant gourmand en registres et l'autre très économe. Dans ce cas, il faudrait partitionner le banc de registre pour donner plus de registre au premier et moins au second. Il s'agit là d'un '''partitionnement dynamique'''.
Le partitionnement dynamique est plus optimal pour gérer des ''shaders'' déséquilibrés niveau registres, mais a une implémentation matérielle plus complexe. Il a été introduit assez tard, car il a fallu attendre que le rayctracing se démocratise. En effet, le partitionnement dynamique du banc de registre est surtout utile pour le raytracing. Exécuter simultanément des shaders déséquilibrés en registres est peu fréquent avec la rastérisation, beaucoup plus courant avec le raytracing.
NVIDIA et AMD ont des implémentations différentes du partitionnement dynamique. Les GPU AMD RDNA 4 allouent un nombre minimal de registre à chaque ''thread'', mais ils peuvent demander d'avoir accès à plus de registres si besoin. Quand un ''thread'' a besoin de plus de registres, il exécute une instruction dédiée, qui sert à demander plus de registres, ou au contraire à en libérer s'ils sont inutilisés. La demande d'allocation de nouveaux registres se fait par blocs de 16 à 32 registres, suivant comment est configuré le processeur. Précisons que l'instruction d'allocation n'est disponible que pour les ''compute shaders'' et pas les ''shaders'' graphiques.
L'instruction d'allocation de registre précédent peut échouer dans certains cas. Si assez de registres sont disponibles, à savoir inutilisés par d'autres ''threads'', l'instruction réussit. Dans le cas contraire, elle échoue et le ''shader'' est mis en pause avant de retenter cette demande plus tard. Le résultat de l'instruction, échec ou réussite, est mémorisé dans le registre d'état. La technique a pour défaut que certaines situations peuvent mener à un blocage complet du processeur, où chaque ''thread'' ne peut plus poursuivre son exécution, faute de registres disponibles. Des méthodes pour éviter cette situation sont implémentés sur ces GPU, mais la documentation n'est pas très explicite.
Sur les GPU NVIDIA, il y a aussi une instruction d'allocation de registre, mais elle fonctionne différemment. Elle permet d'échanger des registres entre ''threads''. Une première différence est que tous les ''threads'' commencent avec une allocation égale des registres. Les ''threads'' démarrent tous avec le même nombre de registres. Un ''thread'' peut libérer des registres, qui sont alors alloués à un autre ''thread'', le ''thread'' en question pouvant être choisit par le ''thread'' qui libère les registres.
===Le banc de registre est multiport de type externe===
Le banc de registre doit permettre de lire deux vecteurs SIMD par opération, soit deux lectures simultanées. Pour cela, le banc de registre contient deux ports de lecture, chacun permettant de lire un opérande dans le banc de registre. Mais plus le nombre de ports augmente, plus la consommation énergétique du banc de registre augmente, sans compter que celui-ci devient plus lent.
Les processeurs peuvent utilisent des bancs de registres ayant réellement deux ports par banc de registre. Un port de lecture est implémenté avec un composant appelé un multiplexeur, connecté à tous les registres.
[[File:Mémoire multiport faite avec des MUX-DEMUX.png|centre|vignette|upright=2|Mémoire multiport faite avec des MUX-DEMUX]]
Les GPU ne peuvent pas se permettre un tel luxe. Leur banc de registre doit alimenter plusieurs unités de calcul en même temps, en parallèle. Le nombre de ports serait plus proche de 4 à 10 ports de lectures. La solution précédente aurait un budget en transistor et un budget thermique trop important. À la place, ils utilisent une autre méthode : ils simulent un banc de registre à plusieurs ports avec un ou plusieurs bancs de registres à un port. On parle alors de '''multiport externe'''.
[[File:Mémoire multiport à multiportage externe.png|centre|vignette|upright=2.5|Mémoire multiport à multiportage externe.]]
Il existe plusieurs méthodes de multiport externe. Mais celle utilisée sur les GPU simule un banc de registre multiport à partir de plusieurs bancs de registres à un port. Le banc de registre est en réalité formé de plusieurs banques, de plusieurs bancs de registre séparés, chacun mono-port. L'idée est que si l'on accède à deux banques en même temps, on peut lire deux opérandes, une par port/banc de registre. Par contre, si les deux opérandes à lire sont la même banque, il y a un '''conflit d'accès aux banques'''.
[[File:Mémoire à multiports par banques.png|centre|vignette|upright=2|Mémoire à multiports par banques.]]
===L'''Operand Collector'' et les caches de ''register reuse''===
Sans conflit d'accès à une banque, les deux opérandes sont disponibles immédiatement. Par contre, en cas de conflit d'accès aux banques, les deux opérandes sont lus l'une après l'autre. En clair, le premier opérande doit être mis en attente quelque part pendant que la seconde est en cours de lecture. Et le problème survient souvent, surtout avec les opérations FMA qui utilisent trois opérandes, encore plus avec les rares opérations qui demandent 4 à 5 opérandes.
Pour gérer les conflits d'accès aux banques, les GPU utilise un circuit dédié, appelé le '''collecteur d'opérandes''' (''operand collector''). Le rôle du collecteur d'opérandes est d'accumuler les opérandes en attente, son nom est assez transparent. Il accumule les opérandes en attente, puis les envoie aux unités de calcul quand elles sont toutes prêtes, toutes lues depuis le banc de registres. Les opérandes sont mis en attente dans des entrées, qui contiennent la donnée, l'identifiant du ''thread''/''warp'' pour ne pas confondre des opérandes entre ''threads'', et deux bits d'occupation. Les deux bits d'occupation indiquent si l'entrée est vide, réservée pour un opérande en cours de lecture ou occupée par un opérande.
Le collecteur d'opérande est souvent accompagné de '''registres temporaires''', qui mémorisent le résultat d'une instruction précédente. Il y a un registre temporaire par ALU, le résultat fournit par une ALU est mémorisé dans le registre temporaire associé. Une instruction peut lire une opérande dans un registre temporaire, ce qui permet de lire le résultat d'une instruction précédente sans passer par le banc de registres. Le collecteur d'opérande est alors configuré pour récupérer les opérandes adéquats dans les registres temporaires adéquats.
: Pour faire une comparaison avec les processeurs modernes, ces registres sont une forme de ''data forwarding'', de contournement, mais qui est rendue explicite pour le logiciel.
Une optimisation des GPU récent vise à réduire les accès aux bancs de registres en utilisant une mémoire cache spécialisée. Il s'agit de l'''operand reuse cache'', aussi appelés '''''register reuse cache'''''. L'idée est que quand un opérande est lu depuis le banc de registres, elle peut être stockée dans ce cache pour des utilisations ultérieures. Les architectures Volta, Pascal et Maxwell disposent de 4 caches de ce type, chacun stockant 8 données/opérandes/résultats
Il s'agit en réalité de pseudo-caches, car ils sont partiellement commandés par le logiciel. Une instruction précise qu'un opérande doit être stocké dans un ''register reuse cache'', pour une utilisation ultérieure. Pour cela, elle incorpore quelques bits pour préciser qu'elle doit être placée dans le cache. Les bits font en quelque sorte partie du mode d'adressage. Si l'instruction immédiatement suivante lit ce registre dans le même ''slot'' d'opérande lira l'opérande dans le cache. La technique est donc assez limitée, mais elle a des résultats pas négligeables.
Les ''register reuse cache'' et le collecteur opérandes sont sans doute fusionnés en un seul circuit, plutôt que d'utiliser deux circuits séparés. La raison est que les deux doivent mémoriser des opérandes les mettre en attente pour une utilisation ultérieure, et sont placés juste après le banc de registre. NVIDIA a publié deux brevets à propos de ces deux techniques, mais rien n'indique que c'est exactement cette technique qui utilisée dans les cartes modernes. Il faut dire que le nombre de banques a changé suivant les cartes graphiques.
* [https://patents.google.com/patent/US7834881B2/en Operand collector architecture ]
* [https://patents.google.com/patent/US20130159628A1/en Methods and apparatus for source operand collector caching ]
{{NavChapitre | book=Les cartes graphiques
| prev=Les processeurs de shader VLIW et DirectX 9
| prevText=Les processeurs de shader VLIW et DirectX 9
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
hlfj4izbkrf8gkj0xjhw81x7uvlwhj9
La grammaire fondamentale de l'ido/Affixes
0
83764
763355
762415
2026-04-09T19:43:00Z
Francucelo
123176
763355
wikitext
text/x-wiki
L'ido peut dériver une infinité de significations différentes et précises à partir d'une racine et d'une multitude de suffixes.
== Suffixes ==
Un suffixe est un élément ajouté à la fin de la racine pour modifier le sens du mot. Il modifie souvent la catégorie du mot, par exemple en transformant une racine adjectivale en verbe.
=== Suffixe de genre ===
En ido, la plupart des noms sont neutres. Pour indiquer le genre, il faut ajouter le suffixe « ul » (masculin) ou « in » (féminin).
* Par exemple, « hom-ul-o » signifie « homme », « hom-in-o » signifie « femme », tandis que « homo » désigne « homme » au sens neutre.
* De même, « frat-ul-o » signifie « frère », « frat-in-o » signifie « sœur », tandis que « frato » désigne de manière neutre « frère ou sœur ».
=== '''Participes''' ===
Le participe est une forme verbale. Dans la pratique, il agit comme un suffixe qui transforme la racine du verbe en un adjectif. Il se distingue selon qu'il est actif ou passif, et selon le temps.
{| class="wikitable"
|+
!
!Actif
!Passif
|-
|'''Présent'''
|ANT
|AT
|-
|'''Passé'''
|INT
|IT
|-
|'''Futur'''
|ONT
|OT
|}
Les particules peuvent être suivies de trois types de terminaisons. Si elles sont rattachées à une racine, elles sont considérées par défaut comme des mots modificatifs.
* Par exemple, « manjanta kato » signifie « un chat qui est en train de manger ».
Les terminaisons des adjectifs et des adverbes servent toutes à exprimer une modification. Cependant, lorsqu'il est suivi de la terminaison « o » , il désigne généralement « une personne qui… » (il s'agit par défaut d'une personne, mais selon le contexte, il peut également s'agir d'un objet concret).
* Par exemple, « manjanto » peut signifier « quelqu'un qui mange ».
=== Principaux suffixes ===
{| class="wikitable"
|+Les principaux suffixes
!Suffixe
!Explication
!Exemple
|-
|ARCH
|De mauvaise qualité, en piteux état
|Dom-arch-o : maison délabrée
|-
|AD
|Mouvement continu
|Parol-ad-o : longue discussion, discours
|-
|AJ
|Choses concrètes
|Manj-aj-o : nourriture
|-
|AL
|En rapport avec
|Liber-al-a : à propos de la liberté
|-
|AN
|Membres, résidents, fidèles
|Urb-an-o : citoyen
|-
|AR
|Ensemble, totalité
|Hom-ar-o : l'Homme ; Arbor-ar-o : forêt
|-
|ARI
|Le destinataire de l'action
|Send-ari-o : récepteur
|-
|ATR
|Comme…
|Flor-atr-a : comme une fleur
|-
|E
|Couleur de…
|Roz-e-a : de couleur rose
|-
|EBL
|Peut être…
|Ir-ebl-a : accessible
|-
|EG
|Énorme, extrême, profond
|Varm-eg-a : caniculaire
|-
|EM
|Tendances, goûts, habitudes
|Labor-em-a : qui aime travailler
|-
|END
|Qui doit être fait
|Fac-end-a : à faire, à régler
|-
|ER
|Amateurs, habitués à faire quelque chose
|Fum-er-o : fumeurs
|-
|ERI
|Lieux, organismes
|Pan-eri-o : boulangerie
|-
|ES
|Concepts abstraits, états
|Liber-es-o : liberté
|-
|ESK
|Commencer
|Dorm-esk-ar : endormir
|-
|ESM
|Nombres ordinaux
|Du-esm-a : deuxième
|-
|ESTR
|Dirigeant, chef
|Urb-estr-o : maire
|-
|ET
|Petit
|Kat-et-o : chaton
|-
|EY
|Un endroit où ranger… ; un espace dédié
|Dorm-ey-o : chambre à coucher
|-
|ID
|descendants, dérivés
|Kat-id-o : descendant du chat
|-
|IF
|produire, fabriquer
|Flor-if-ar : fleurir
|-
|IG
|Rendre
|Bel-ig-ar : embellir
|-
|IJ
|Devenir
|Rich-ij-ar : Devenir riche
|-
|IL
|Outils, instruments
|Skrib-il-o : outil d'écriture, stylo
|-
|IND
|Vaut la peine…
|Respekt-ind-a : digne de respect
|-
|ISM
|idéologies, doctrines, systèmes, religions
|Liber-ism-o : libéralisme
|-
|IST
|Professionnel, adepte
|Art-ist-o : artiste
|-
|IV
|Pouvoir… (actif)
|Instrukt-iv-a : éducatif, instructif
|-
|IZ
|Attribuer, couvrir, équiper
|Sal-iz-ar : saler
|-
|OZ
|rempli de, riche en
|Felic-oz-a : plein de joie
|}
== Préfixes ==
Les préfixes sont des affixes placés devant la racine pour modifier son sens.
=== Prépositions ===
En ido, de nombreuses prépositions peuvent être utilisées comme préfixes.
* Par exemple, « Sur-tabla telefonilo » signifie « téléphone sur la table », et transforme « sur la table » en un seul adjectif.
=== Principaux préfixes ===
{| class="wikitable"
|+Les principaux préfixes
!Préfixe
!Explication
!Exemple
|-
|ANTI
|S'opposer, résister
|Anti-bakteria : antibactérien
|-
|ARKI
|Le plus haut, le premier (en importance)
|Arki-regulo : règles fondamentales
|-
|AUTO
|Automatique, soi-même
|Auto-biografio : autobiographie
|-
|BO
|Liens familiaux
|Bo-patro : beau-père
|-
|DES
|Antonyme direct
|Des-granda : petit
|-
|DIS
|Diffusion
|Dis-semar : semer, disperser les graines
|-
|EX
|L'ex, l'ancien
|Ex-prezidanto : ancien président
|-
|GE
|Commun aux deux sexes
|Ge-patri : parents
|-
|MI
|La moitié
|Mi-horo : demi-heure
|-
|MIS
|À tort, de manière inappropriée
|Mis-komprenar : malentendu
|-
|NE
|Négation
|Ne-bona : pas bon
|-
|PAR
|Complètement, entièrement
|Par-lektar : lire entièrement
|-
|PSEUDO
|Faux, contrefait
|Pseudo-nomo : pseudonyme
|-
|RETRO
|En arrière, retour
|Retro-irar : reculer
|-
|RI
|À nouveau
|Ri-facar : refaire
|-
|SEN
|Sans
|Sen-viva : sans vie
|}
Il est important de faire la distinction entre les préfixes DES et NE. Le premier exprime une antonymie directe, tandis que le second n'indique qu'une simple négation.
{{AutoCat}}
nvrawpfmaarmp6gcafhqyrr01n9bt9t
Les cartes graphiques/Les processeurs de shader VLIW et DirectX 9
0
83790
763320
2026-04-09T15:02:27Z
Mewtow
31375
Page créée avec « Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait ap... »
763320
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Les processeurs VLIW sur les cartes graphiques AMD==
Les cartes graphiques AMD et ATI d'architectures R300, de la série des Radeon 9700, étaient des processeurs VLIW. L'intérêt du VLIW était de faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles pouvaient faire plusieurs types d'opérations : des opérations SIMD sur des vecteurs de 4 flottants, des opérations SIMD sur des vecteurs de 4 entiers, des opérations scalaires sur des entiers, et des opérations scalaires sur des flottants. Il n'y avait que deux opérations simultanées possibles : une vectorielle et une scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
* L'opération vectorielle pouvait aussi bien manipuler des vecteurs de flottants que d'entiers. Elle gérait les opérations de base, à savoir : comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Elle gérait aussi des instructions CMOV, mais aussi et surtout des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
* L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits, soit les classiques comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Ajoutons à cela une opération CMOV.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. L'opération scalaire est maintenant uniquement flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
* 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
* une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
==Un cas particulier : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners'' vus dans le chapitre précédent. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
5s1bts2tnje0tte631jk9rxrbo2hdj3
763323
763320
2026-04-09T15:03:09Z
Mewtow
31375
/* L'abandon des architectures VLIW */
763323
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Les processeurs VLIW sur les cartes graphiques AMD==
Les cartes graphiques AMD et ATI d'architectures R300, de la série des Radeon 9700, étaient des processeurs VLIW. L'intérêt du VLIW était de faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles pouvaient faire plusieurs types d'opérations : des opérations SIMD sur des vecteurs de 4 flottants, des opérations SIMD sur des vecteurs de 4 entiers, des opérations scalaires sur des entiers, et des opérations scalaires sur des flottants. Il n'y avait que deux opérations simultanées possibles : une vectorielle et une scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
* L'opération vectorielle pouvait aussi bien manipuler des vecteurs de flottants que d'entiers. Elle gérait les opérations de base, à savoir : comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Elle gérait aussi des instructions CMOV, mais aussi et surtout des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
* L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits, soit les classiques comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Ajoutons à cela une opération CMOV.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. L'opération scalaire est maintenant uniquement flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
* 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
* une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
==Un cas particulier : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners'' vus dans le chapitre précédent. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ai3r641l2xoofe08tmg9vqieuajov04
763326
763323
2026-04-09T15:04:06Z
Mewtow
31375
/* Un cas particulier : les register combiners */
763326
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Les processeurs VLIW sur les cartes graphiques AMD==
Les cartes graphiques AMD et ATI d'architectures R300, de la série des Radeon 9700, étaient des processeurs VLIW. L'intérêt du VLIW était de faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles pouvaient faire plusieurs types d'opérations : des opérations SIMD sur des vecteurs de 4 flottants, des opérations SIMD sur des vecteurs de 4 entiers, des opérations scalaires sur des entiers, et des opérations scalaires sur des flottants. Il n'y avait que deux opérations simultanées possibles : une vectorielle et une scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
* L'opération vectorielle pouvait aussi bien manipuler des vecteurs de flottants que d'entiers. Elle gérait les opérations de base, à savoir : comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Elle gérait aussi des instructions CMOV, mais aussi et surtout des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
* L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits, soit les classiques comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Ajoutons à cela une opération CMOV.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. L'opération scalaire est maintenant uniquement flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
* 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
* une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners'' vus dans le chapitre précédent. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
njs5ba0etcw9g403v0x08xl30pfi04d
763327
763326
2026-04-09T15:04:28Z
Mewtow
31375
/* Avant les pixel shaders : les register combiners */
763327
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Les processeurs VLIW sur les cartes graphiques AMD==
Les cartes graphiques AMD et ATI d'architectures R300, de la série des Radeon 9700, étaient des processeurs VLIW. L'intérêt du VLIW était de faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles pouvaient faire plusieurs types d'opérations : des opérations SIMD sur des vecteurs de 4 flottants, des opérations SIMD sur des vecteurs de 4 entiers, des opérations scalaires sur des entiers, et des opérations scalaires sur des flottants. Il n'y avait que deux opérations simultanées possibles : une vectorielle et une scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
* L'opération vectorielle pouvait aussi bien manipuler des vecteurs de flottants que d'entiers. Elle gérait les opérations de base, à savoir : comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Elle gérait aussi des instructions CMOV, mais aussi et surtout des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
* L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits, soit les classiques comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Ajoutons à cela une opération CMOV.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. L'opération scalaire est maintenant uniquement flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
* 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
* une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ceec0155pq4plm8chr2xw8lockk3cg8
763331
763327
2026-04-09T15:09:56Z
Mewtow
31375
/* Les processeurs VLIW sur les cartes graphiques AMD */
763331
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Les processeurs VLIW sur les cartes graphiques AMD==
Les cartes graphiques AMD et ATI d'architectures R300, de la série des Radeon 9700, étaient des processeurs VLIW. L'intérêt du VLIW était de faciliter les calculs de produits vectoriels combinés avec de l'éclairage. Elles pouvaient faire plusieurs types d'opérations : des opérations SIMD sur des vecteurs de 4 flottants, des opérations SIMD sur des vecteurs de 4 entiers, des opérations scalaires sur des entiers, et des opérations scalaires sur des flottants. Il n'y avait que deux opérations simultanées possibles : une vectorielle et une scalaire. Les contraintes de combinaisons des instructions sont assez complexes.
L'opération vectorielle pouvait aussi bien manipuler des vecteurs de flottants que d'entiers. Elle gérait les opérations de base, à savoir : comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Elle gérait aussi des instructions CMOV, mais aussi et surtout des multiplications et additions flottante, une opération MAD, des produits vectoriels ou scalaires, et diverses opérations d'arrondis ou de conversion entre flottants.
L'opération scalaire était : soit une opération de conversion entier-flottant, soit une opération transcendantale (entière ou flottante), soit une multiplication entière 32 bits, soit une multiplication flottante 32 bits, soit les classiques comparaisons, additions, soustractions, opérations logiques et bit à bit, décalages. Ajoutons à cela une opération CMOV.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf]
Par la suite, les cartes graphiques AMD ont changé les possibilités de combinaisons entre opérations. L'opération scalaire est maintenant uniquement flottante, la possibilité de faire une opération entière a été retirée. La première opération est soit une opération transcendantale, soit une opération vectorielle sur trois flottants. L'origine de ce changement, peu intuitif, sera expliqué dans le chapitre sur la microarchitecture des processeurs de shaders. Pour résumer, le processeur peut faire au choix :
* 4 opérations flottantes en parallèle : 3 calculs flottants via SIMD, plus un par l’opération scalaire.
* une opération transcendantale couplée à une opération flottante.
Le tout donna une architecture appelée par AMD : VLIW-4. 4, car le processeur peut faire au grand max 4 opérations flottantes en parallèle.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
mccjloegwtu4p1f4613mshkncoyhtt5
763332
763331
2026-04-09T15:11:47Z
Mewtow
31375
/* Les processeurs VLIW sur les cartes graphiques AMD */
763332
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les architectures VLIW pures, sans unité SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était une architecture VLIW, pas SIMD, d'où la distinction. Il n'y avait pas d'unité de calcul SIMD, mais plusieurs unités de calcul scalaires. Un point important est que les unités de calculs scalaires pouvaient faire des opérations différentes. Par exemple, la première pouvait faire une addition flottante, la seconde une addition entière, la troisième une soustraction, etc.
Elles disposaient de six unités de calcul : cinq unités de calcul scalaires, et une unité de calcul dédiée aux branchements. Sur les 5 unités de calcul, une était une unité de calcul hybride scalaire/transcendentale. Nous allons donc faire la distinction entre unité de calcul spéciale et les 4 unités de calcul basiques.
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
l9rghv5peq9cn2fq2goips82qmhi58b
763333
763332
2026-04-09T15:29:24Z
Mewtow
31375
/* Les architectures VLIW pures, sans unité SIMD */
763333
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
g3udbetx82d8sff6ckb6cjmmkmkhwql
763334
763333
2026-04-09T15:29:49Z
Mewtow
31375
/* Les microarchitectures Terascale d'AMD */
763334
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les microarchitectures Terascale d'AMD : un bybride VLIW-SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les hybrides SIMD/VLIW et les instructions à ''co-issue''===
Les processeurs VLIW plus évolués étaient des hybrides SIMD/VLIW qui pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération scalaire. Cependant, n'imaginez pas que l'unité de calcul scalaire existait déjà. Il n'y avait pas de séparation entre composants RGB et composante alpha, les quatre étaient envoyées à l'unité SIMD. L'opération scalaire, exécutée en parallèle de l'opération SIMD, était une instruction transcendantale. En clair, seule une forme plus limitée de ''co-issue'' était possible : on pouvait exécuter une opération transcendantale en parallèle d'une instruction SIMD. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6880. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul scalaire flottante, qui gère les opérations flottantes transcendantales. Elle travaille sur des opérandes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
ab54giu1vf2q1m3y7tr9wypjhwoj8nv
763335
763334
2026-04-09T15:35:36Z
Mewtow
31375
/* Les hybrides SIMD/VLIW et les instructions à co-issue */
763335
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les microarchitectures Terascale d'AMD : un bybride VLIW-SIMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul transcendantale, qui prend des opérandes flottantes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
7puf9ajny3mj5rhlp6tlrho0ocwvg32
763336
763335
2026-04-09T15:35:44Z
Mewtow
31375
/* Les microarchitectures Terascale d'AMD : un bybride VLIW-SIMD */
763336
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Sur les processeurs de shaders anciens, on pouvait regrouper 4 opérations : trois opérations identiques, et une quatrième. Pour simplifier, cette technique permettait d’exécuter une opération appliquée aux couleurs R, G, et B, et une autre qui sera appliquée à la couleur de transparence. Il s'agit donc d'un mix entre une unité SIMD et une unité scalaire. En plus de cela, le processeur acceptait souvent d'exécuter une opération transcendantale dans l'unité de calcul spéciale, en parallèle des autres. Les processeurs de l'époque avaient donc : une unité SIMD, une unité scalaire regroupant ALU et FPU, et une unité transcendantale.
Du moins, c'est la théorie, car certaines cartes graphiques faisaient autrement. Il arrivait que l'unité SIMD était scindée en plusieurs unités scalaires, chacune pouvant effectuer un calcul différent sur chaque composant RGB. Pour le moment, concentrons-nous sur les processeurs de shaders VLIW sans aucune unité de calcul SIMD.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul transcendantale, qui prend des opérandes flottantes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
plch03ful70fux6p12t1orf59mzmqzk
763337
763336
2026-04-09T15:38:56Z
Mewtow
31375
/* Les processeurs VLIW pour les shaders proprement dit */
763337
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Les processeurs de shader de l'époque de DirectX 9 étaient des hybrides entre VLIW et SIMD. La manière dont les deux étaient hybridés dépendait du GPU. Une première solution était d'utiliser plusieurs processeurs VLIW, mais de leur faire exécuter une instruction identique à chaque cycle. C'est la solution qui a été retenue sur les GPU AMD de microarchitecture Terascale. Une autre solution est d'utiliser des processeurs VLIW, dont les faisceaux permettaient de regrouper une instruction SIMD avec une opération scalaire. Elle a été utilisé sur les GPOU NVIDIA à partir de la Geforce 6800. Nous allons voir les deux techniques dans le détail dans ce qui suit.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ""vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul transcendantale, qui prend des opérandes flottantes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
mhcn95ijim9sc3wtb4989vlp0q4q08q
763339
763337
2026-04-09T15:53:08Z
Mewtow
31375
/* Les cartes graphiques NVIDIA 6800 */
763339
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Les processeurs de shader de l'époque de DirectX 9 étaient des hybrides entre VLIW et SIMD. La manière dont les deux étaient hybridés dépendait du GPU. Une première solution était d'utiliser plusieurs processeurs VLIW, mais de leur faire exécuter une instruction identique à chaque cycle. C'est la solution qui a été retenue sur les GPU AMD de microarchitecture Terascale. Une autre solution est d'utiliser des processeurs VLIW, dont les faisceaux permettaient de regrouper une instruction SIMD avec une opération scalaire. Elle a été utilisé sur les GPOU NVIDIA à partir de la Geforce 6800. Nous allons voir les deux techniques dans le détail dans ce qui suit.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous. On voit que le processeur contient une unité d'accès mémoire/textures, avec deux unités de calcul. La première est une unité de calcul transcendantale, qui prend des opérandes flottantes de 32 bits. À côté, on trouve une unité de calcul SIMD, qui gère des vecteurs de 4 nombres flottants 32 bits. Elle permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Il n'y a aucune gestion d'instructions flottantes scalaires ou d'opérations entières, la carte graphique est trop ancienne pour cela.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de ''pixel shader'' de la Geforce 6800. Il disposait de plusieurs unités de calcul utilisables en parallèle. La première est une unité de calcul capable de réaliser une multiplication et une addition SIMD, portant sur des vecteurs de 4 éléments. La seconde effectue des fonctions arithmétiques spéciales, comme les logarithmes, exponentielles ou les calculs trigonométriques, les produits scalaires et autres. Enfin, il y a une unité d'accès aux textures, ce qui veut dire que le ''vertex shader'' a la possibilité de lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité de calcul multiplication/addition était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
5ulskcg4ajtrxayi4fdxzjnns178ng1
763340
763339
2026-04-09T15:57:21Z
Mewtow
31375
/* Les cartes graphiques NVIDIA 6800 */
763340
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Les processeurs de shader de l'époque de DirectX 9 étaient des hybrides entre VLIW et SIMD. La manière dont les deux étaient hybridés dépendait du GPU. Une première solution était d'utiliser plusieurs processeurs VLIW, mais de leur faire exécuter une instruction identique à chaque cycle. C'est la solution qui a été retenue sur les GPU AMD de microarchitecture Terascale. Une autre solution est d'utiliser des processeurs VLIW, dont les faisceaux permettaient de regrouper une instruction SIMD avec une opération scalaire. Elle a été utilisé sur les GPOU NVIDIA à partir de la Geforce 6800. Nous allons voir les deux techniques dans le détail dans ce qui suit.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 disposait une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et d'une unité de calcul SIMD. L'unité SIMD permet de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres.
Intuitivement, on se dit que le processeur est capable de faire une opération SIMD, une opération d'accès aux textures, et une opération transcendantale. Sauf qu'en réalité, l'unité SIMD était beaucoup plus flexible. Elle permettait de faire : soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants en ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune. En clair, il s'agissait plus d'un regroupement VLIW de quatres ALU flottantes, mais la documentation de NVIDIA la qualifie d'unité SIMD, ce qui est très bizarre...
La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''. Le processeur de ''vertex shader'' de la Geforce 6800 est illustré ci-dessous.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader de la même carte graphique était lui aussi un hybride entre VLIW et SIMD. Cependant, il lorgnait plus du côté VLIW que SIMD. Il pouvait faire soit une opération SIMD sur un vecteur, soit couper le vecteur en deux et effectuer deux opérations différentes sur chaque morceau. Par exemple, il pouvait faire une opération sur 3 pixels, et une opération scalaire sur le quatrième, ou deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
bmmkps1irvcvbyuzih4amuinkwln6ht
763341
763340
2026-04-09T16:01:02Z
Mewtow
31375
/* Les cartes graphiques NVIDIA 6800 */
763341
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Les processeurs VLIW pour les ''shaders'' proprement dit==
Les processeurs de shader de l'époque de DirectX 9 étaient des hybrides entre VLIW et SIMD. La manière dont les deux étaient hybridés dépendait du GPU. Une première solution était d'utiliser plusieurs processeurs VLIW, mais de leur faire exécuter une instruction identique à chaque cycle. C'est la solution qui a été retenue sur les GPU AMD de microarchitecture Terascale. Une autre solution est d'utiliser des processeurs VLIW, dont les faisceaux permettaient de regrouper une instruction SIMD avec une opération scalaire. Elle a été utilisé sur les GPOU NVIDIA à partir de la Geforce 6800. Nous allons voir les deux techniques dans le détail dans ce qui suit.
===Les microarchitectures Terascale d'AMD===
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
===Les cartes graphiques NVIDIA 6800===
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 était un processeur VLIW. Il disposait une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et de 4 unités de calcul flottantes. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Les unités flottantes permettaient de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Les 4 unités flottantes étaient regroupées en un seul paquet, mais dont l'allocation était très flexible. Elles permettaient de faire soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants en ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Il pouvait faire soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
9sibtwkg9fr9w6fa46mgiesn4lpelsg
763342
763341
2026-04-09T16:02:13Z
Mewtow
31375
763342
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==Les microarchitectures Terascale d'AMD==
Il a existé des processeurs de shader sans unité SIMD, qui remplacaient celle-ci par plusieurs ALU/FPU séparées. Un exemple est celui des anciennes cartes graphiques AMD, d'architecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
==Les GPU NVIDIA 6800==
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 était un processeur VLIW. Il disposait une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et de 4 unités de calcul flottantes. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Les unités flottantes permettaient de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Les 4 unités flottantes étaient regroupées en un seul paquet, mais dont l'allocation était très flexible. Elles permettaient de faire soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants en ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Il pouvait faire soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
k7250ccvk9j0kmmquagdvcmbr7ujjwf
763343
763342
2026-04-09T16:02:38Z
Mewtow
31375
/* Les microarchitectures Terascale d'AMD */
763343
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==Les microarchitectures Terascale d'AMD==
Voyons maintenant l'exemple des GPU AMD de microarchitecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
==Les GPU NVIDIA 6800==
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 était un processeur VLIW. Il disposait une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et de 4 unités de calcul flottantes. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
Les unités flottantes permettaient de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. Les 4 unités flottantes étaient regroupées en un seul paquet, mais dont l'allocation était très flexible. Elles permettaient de faire soit une opération SIMD agissant sur des vecteurs de 4 éléments, soit une opération vectorielle sur 3 flottants en ''co-issue'' avec une opération scalaire, deux opérations vectorielles sur deux éléments chacune.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Il pouvait faire soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
0yd7cp6898is3e53b2vcbzekazpz4ju
763344
763343
2026-04-09T16:08:54Z
Mewtow
31375
/* Les GPU NVIDIA 6800 */
763344
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==Les microarchitectures Terascale d'AMD==
Voyons maintenant l'exemple des GPU AMD de microarchitecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
==Les GPU NVIDIA 6800==
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 disposait : d'une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et d'une unité de calcul SIMD. L'unité SIMD permettait de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Il pouvait faire soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels. Le processeur travaille sur des blocs de 4 pixels, appelés des ''quads''. Chaque pixel est codé avec 4 flottants 32 bits, cela fait en tout 4 vecteurs de 4 flottants, avec ''co-issue'' à l'intérieur de chaque vecteur. Précisons que les registres temporaires du processeur mémorisent chacun un vecteur de 4 flottants, un pixel, par un ''quad''.
Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, mais elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. La documentation NVIDIA elle-même parle d'unité vectorielle, mais le terme est quelque peu trompeur, car elles sont plus flexibles que de simples unités SIMD.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
npryc2lwwelskp5nwp5bznq28p9vvsc
763345
763344
2026-04-09T16:13:33Z
Mewtow
31375
/* Les GPU NVIDIA 6800 */
763345
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==Les microarchitectures Terascale d'AMD==
Voyons maintenant l'exemple des GPU AMD de microarchitecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
==Les GPU NVIDIA 6800==
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 disposait : d'une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et d'une unité de calcul SIMD. L'unité SIMD permettait de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, histoire de suivre la documentation NVIDIA, mais le terme est quelque peu trompeur, car elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. Chaque unité SIMD pouvait soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==L'abandon des architectures VLIW==
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
Les processeurs de shader disposent de circuits de calcul séparés, appelés des unités de calcul. Chaque unité de calcul peut faire des additions/soustractions/comparaisons, parfois des multiplications, voire d'autres opérations. Et avec le VLIW, chaque unité de calcul fonctionne séparément des autres, elles peuvent effectuer chacun un calcul différent des autres unités de calcul. L'ensemble est beaucoup plus flexible qu'avec le SIMD, où toutes les unités de calcul doivent faire le même calcul.
Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions, qu'on abordera dans quelques chapitres. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
apqgisnci3culjbo80ibts28r8vu9ub
763346
763345
2026-04-09T16:15:43Z
Mewtow
31375
/* L'abandon des architectures VLIW */
763346
wikitext
text/x-wiki
Les deux chapitres précédents ont abordé les processeurs de shader modernes, de l'époque DirectX 10 et après. Mais pour les GPU avant DirectX 9, les processeurs de shaders étaient totalement différents. Ils n'étaient pas des processeurs de shaders de type SIMD, mais des processeurs de type VLIW. AMD a utilisé des processeurs VLIW sur sa microarchitecture Terascale, avant le passage aux processeurs SIMD avec l'architecture GCN en 2012. NVIDIA utilisait apparemment aussi des processeurs VLIW sur les Geforce 3, 4 et FX, 6 et 7. Globalement, les processeurs de shader VLIW datent de l'ère de Dirext 9, et ont été abandonnés avec l'arrivée de DirextX 10/11.
==Les processeurs VLIW : généralités==
Pour rappel, un processeur de shader incorpore plusieurs unités de calcul, avec typiquement une unité de calcul SIMD, une ALU scalaire, une FPU scalaire, et potentiellement une unité transcendantale. Pour les exploiter au mieux, l'idéal serait de lancer une opération différente dans chaque unité de calcul. Les CPU le permettent en utilisant des optimisations comme l'exécution superscalaire, l'exécution dans le désordre, etc. Les processeurs de shader de type SIMD le permettent avec les instructions en ''co-issue'', mais c'est une solution assez limitée. Les processeurs VLIW, quant à eux, poussent l'utilisation de la ''co-issue'' à des niveaux extrêmes, en exposant les unités de calcul au programmeur.
Une instruction VLIW encode plusieurs opérations, chaque opération allant dans une unité de calcul différente ! Les processeurs VLIW regroupent plusieurs instructions dans des sortes de super-instructions appelées des '''faisceaux d'instruction''' (aussi appelés ''bundle''). Là où les instructions machines usuelles effectuent une seule opération, les faisceaux d'instruction VLIW exécutent plusieurs opérations indépendantes en même temps, dans des unités de calcul séparées. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique.
[[File:Vliwpipeline.svg|centre|vignette|upright=1.5|Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.]]
Il y a de nombreuses contraintes quant au regroupement des opérations. On ne peut pas regrouper n'importe quelle opération avec n'importe quelle autre, il faut que les unités de calcul permettent le regroupement. Prenons l'exemple d'un processeur VLIW disposant d'une ALU entière et d'une FPU : il sera possible de regrouper une opération entière avec une opération flottante, mais pas de regrouper deux opérations flottantes ou deux opérations entières. Il y a aussi des contraintes sur les registres : les instructions d'un faisceau ne peuvent pas écrire dans les mêmes registres, il y a des contraintes qui font que si telle opération utilise tel registre, alors certains autres registres seront interdits pour l'autre opération, etc.
Les opérations regroupées sont garanties indépendantes par le compilateur, ce qui fait que le décodeur d'instruction envoie chaque opération à l'unité de calcul associée, sans avoir à faire la moindre vérification de dépendances entre instructions. L'unité d'émission est donc grandement simplifiée, elle n'a pas à découvrir les dépendances entre instructions.
Au passage, cela explique pourquoi les premières cartes graphiques étaient de type VLIW, alors que les modernes sont de type SIMD. Les anciennes cartes graphiques préféraient se passer de ''scoreboard'', afin d'utiliser le peu de transistors dont elles disposaient pour des unités de calcul. De plus, DirectX 8 et 9 profitaient pas mal de la présence de ''co-issue''. Par contre, il fallait un compilateur performant pour en profiter, ce qui n'était pas vraiment le cas. Les compilateurs n'exploitaient pas beaucoup la ''co-issue'', ce qui fait que les fabricant de GPU ont préféré déléguer cette tâche à un ''scoreboard'' matériel.
==Avant les ''pixel shaders'' : les ''register combiners''==
La toute première utilisation de processeurs VLIW sur un GPU était la Geforce 256, avec l'usage des ''register combiners''. Pour rappel, les ''register combiners'' sont des opérations qui permettaient de mélanger plusieurs textures entre elles, le mélange étant partiellement programmable. Pour cela, les cartes graphiques de l'époque de Direct X 6 incorporaient un processeur VLIW très particulier.
Il disposait d'un nombre limité de registres, une dizaine en tout. Lors d'une opération de ''multitexturing'', les registres étaient initialisés avec les données adéquates, lues depuis les textures ou fournies par l'unité de rastérisation. Quelques registres étaient en lecture seule, d'autres étaient modifiables par les instructions VLIW. Les registres contiennent tous des couleurs au format RGB-A, à savoir une couleur RGB codée sur trois entiers, et une composante de transparence codée avec un entier.
Les quatre registres constants, en lecture seule, sont les suivants :
* un '''registre zéro''', contenant toujours 0 ;
* un '''registre ''fog''''' contenant la couleur du brouillard ;
* deux '''registres de couleur configurables''' par l'utilisateur.
Les registres modifiables sont les suivants :
* Des '''registres de texel''', un par unité de texture, qui mémorise le texel lu lors du placage de texture ;
* Des '''registres généraux''' qui n'ont pas de fonction prédéterminée ;
* Deux '''registres d'éclairage par sommet''' qui mémorisent respectivement les couleurs spéculaire et diffuse fournies par l'unité de rastérisation.
L'implémentation du circuit est inconnue, mais son interface l'est très bien. Tout se passe comme si le processeur incorporait deux unités de calcul : une appelée l'unité RGB, l'autre appelée l'unité alpha. Leur nom trahit ce qu'elles font : l'une calcule un résultat RGB, l'autre calcule la composant de transparence alpha. Elles fonctionnent en parallèle, ce qui fait qu'elles peuvent faire deux opérations simultanément. Enfin, opération, le terme est vite dit car chaque unité de calcul peut faire plusieurs opérations simultanées.
Les deux unités prennent quatre opérandes notées A, B, C et D. Ce sont sont des opérandes flottantes codées sur 32 bits, qui peuvent être lues dans tous les registres. Rappelons cependant qu'un registre contient 4 flottants : trois pour le codage d'une couleur RGB, un autre pour la transparence A. Les opérandes n'ont pas à provenir du même registre. Par exemple, il est parfaitement possible de lire la composant A d'un registre, la composant R d'un second registre, et les composantes R V d'un troisième.
Intuitivement, on s'attend à ce que l'unité RGB lise les registres R, G et B, et écrire ses résultats dans les mêmes registres. Et pour l'unité alpha, on s'attend à ce qu'elle prenne ses opérandes dans les registres A et écrive ses résultats dans les mêmes registres. Mais ce n'est pas du tout ce qui se passe. L'unité RGB peut lire les registres R, G et B, mais aussi les registres A pour la transparence. Il peut lire toutes les composantes de tous les registres, sauf un : la composant alpha du registre de brouillard. Pour l'unité alpha, elle peut lire les registres A pour la transparence, mais elle peut aussi lire les couleurs bleues, la portion B d'un registre RGBA. En clair, sur les registres RGBA, les registres B et A servent comme opérande pour l'unité alpha.
L'unité alpha est capable de faire des multiplications, deux multiplications à la fois. Elle peut faire trois opérations en même temps et possède donc trois sorties. La première sortie fournit le résultat de la multiplication A*B, la seconde sortie le produit C*D. La troisième sortie est plus complexe. Elle peut faire deux opérations. La première fait l'addition des deux produits A * B + C * D. La seconde fournit soit A * B, soit C * D, suivant la valeur de transparence du registre de texture voulu : elle fournit A*B si l'alpha de la texture est supérieur à 0.5, C*D sinon. Pour mieux comprendre son fonctionnement, voici une implémentation possible :
[[File:Implementation de l'unité alpha des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité alpha des registers combiners]]
L'unité RGB est capable de faire des produits scalaires en plus des multiplications. Elle prend quatre opérandes entières notées A, B, C et D, qui peuvent être lues dans tous les registres, et peuvent être lues à la fois dans les portions RGB et A d'un registre. Elle peut faire maximum trois opérations en même temps et possède donc trois sorties. Les deux premières sorties peuvent fournir soit un produit scalaire, soit une multiplication. La troisième sortie ne change pas comparé à l'unité alpha, mais elle est désactivée si l'unité RGB effectue au moins un produit scalaire.
[[File:Implementation de l'unité RGB des registers combiners.png|centre|vignette|upright=2|Implementation de l'unité RGB des registers combiners]]
Il faut noter que les sorties des deux unités de calcul sont connectées à une mini-ALU qui met à l'échelle le résultat. La mise à l'échelle multiplie les trois résultats par 0.5, 1, 2, 4, au choix. De plus, le résultat peut subir une soustraction spécifique, à savoir qu'on peut lui retirer 0.5, mais seulement si on multiplie le résultat par 1 et 2.
{|class="wikitable"
|-
! Multiplication !! Biais facultatif
|-
| 0.5 || X
|-
| 1 || - 0.5 facultatif
|-
| 2 || -0.5 facultatif
|-
| 4 || X
|}
==Les microarchitectures Terascale d'AMD==
Voyons maintenant l'exemple des GPU AMD de microarchitecture TeraScale/VLIW-5, à savoir les Radeon HD 2000/3000/4000/5000/6000. L'architecture était un hybride entre VLIW et SIMD. Précisément, un processeur de shader Terascale contenait 16 cœurs VLIW, qui fonctionnaient en ''lockstep'' : tous les processeurs VLIW exécutaient la même instruction, mais sur des données différentes. L'instruction en question était une instruction VLIW, exécutée par un cœur VLIW.
Un cœur VLIW est un chemin de données à part. Tous les cœurs VLIW partagent la même unité de contrôle. Un cœur VLIW regroupe des registres, 4 unités de calcul, une unité de calcul transcendantal et une unité de branchement. Les unités de calcul faisaient à la fois ALU et FPU, et on peut supposer qu'il y avait en réalité 4 ALU et 4 FPU, regroupées en 4 paires ALU+FPU, de manière à ce que l'on ne puisse pas à la fois utiliser une ALU et une FPU d'une même paire.
Le tout était appelé du VLIW-5 par AMD/ATI. VLIW-5, car on pouvait effectuer 4 calculs flottants en parallèle avec l’opération SIMD d'un cinquième (entier ou flottant). Avec 16 cœurs VLIW, chacun pouvant faire 4 opérations entières/flottantes, on pouvait donc exécuter en une fois 64 opérations d'un seul coup + 16 opérations transcendantales. Le jeu d'instruction est rendu public dans la documentation d'AMD, le voici : [https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/R700-Family_Instruction_Set_Architecture.pdf].
Toutes les unités de calculs pouvaient faire les opérations suivantes, sur des flottants et entiers sur 32 bits : comparaisons, additions, soustractions, opérations logiques, décalages, opérations bit à bit et instructions CMOV. Les unités de calcul basiques gèrent aussi les multiplications, opérations MAD et produits vectoriels/scalaires, mais seulement pour des opérandes flottantes. L'unité de calcul spéciale gérait des multiplications et division entières sur des opérandes 32 bits, ainsi que des instructions transcendantales entières/flottantes.
Par la suite, avec l'architecture VLIW-4, l'unité de calcul transcendantale a été retirée. Mais les calculs transcendantaux n'ont pas disparus. En effet, il ne resta que 4 ALU flottantes, qui ont été augmentées pour gérer partiellement les opérations transcendantales. Tout se passait comme si l'ALU transcendantale avait été éclatée en morceaux répartis dans chaque ALU flottante/entière. Et c'est globalement ce qui s'est passé : les diverses tables matérielles utilisées pour les calculs transcendantaux ont été dispersés dans les ALU, afin de faire des calculs transcendantaux approchés. En combinant les résultats approchés, on pouvait calculer le résultat exact.
==Les GPU NVIDIA 6800==
D'autres processeurs de shaders pouvaient exécuter une opération SIMD en ''co-issue'' avec une opération transcendantale. Un exemple est le processeur de ''vertex shader'' de la Geforce 3, qui a une unité SIMD et une unité transcendantale. Il n'y avait pas d'unité de calcul scalaire entière, ni même flottante, juste l'unité transcendantale et une unité SIMD.
Un autre exemple, que nous allons étudier en détail, est le processeur de vertices de la Geforce 6800. Ses processeurs de ''vertex shader'' pouvaient faire une opération SIMD sur des flottants de 32 bits, en ''co-issue'' avec une opération transcendantale sur des flottants de 32 bits. Par contre, ses processeurs de pixel shader avaient des possibilités de ''co-issue'' plus développées. Et nous allons voir les deux l'un après l'autre.
Le processeur de ''vertex shader'' de la Geforce 6800 disposait : d'une unité d'accès mémoire/textures, d'une unité de calcul transcendantale, et d'une unité de calcul SIMD. L'unité SIMD permettait de faire des additions, des multiplications, des opérations MAD, des produits vectoriels, et quelques autres opérations comme le calcul du maximum/minimum de deux nombres. La présence d'une unité d'accès aux textures implique que le ''vertex shader'' peut lire des textures en mémoire vidéo, ce qui facilite l'implémentation de certaines techniques de rendu. On remarque aussi la présence d'un cache de texture intégré dans le processeur de ''vertex shader''.
[[File:GeForce 6800 Vertex processor block.png|centre|vignette|upright=2|GeForce 6800 Vertex processor.]]
Le processeur de pixel shader, quant à lui, avait globalement les mêmes capacités. Niveau unités de calcul, le tout était assez complexe. Il contenait tout d'abord une unité de texture, et plusieurs unités VLIW/SIMD. Elles sont appelées "unités SIMD" dans les schémas qui vont suivre, histoire de suivre la documentation NVIDIA, mais le terme est quelque peu trompeur, car elles sont en réalité un mix entre unité SIMD véritable et unités scalaires du VLIW. Chaque unité SIMD pouvait soit une opération SIMD sur un vecteur, soit faire une opération sur 3 pixels et une opération scalaire sur le quatrième, soit deux opérations vectoriels chacune sur deux pixels.
Il y en a deux, la première envoyant son résultat à la seconde. La première est capable de faire des opérations de multiplications/MAD, mais elle peut aussi être utilisée pour la correction de perspective grâce à son support des opérations 1/x. Elle peut aussi normaliser des nombres flottants sur 16 bits. Il faut noter que la première unité SIMD/VLIW ne peut pas être utilisée si un accès mémoire/texture est en cours. La seconde unité SIMD/VLIW est capable de faire des opérations de MAD, et un produit vectoriel/scalaire DOT4.
Le résultat de la seconde ALU est ensuite envoyé à une unité de branchement qui décide s'il faut ré-exécuter une autre passe d'instructions ou non. Il s'agit vraisemblablement d'une unité qui gère la prédication des résultats. Une fois le fragment/pixel final calculé, il est envoyé à une unité de calcul du brouillard, qui est une unité de calcul spécialisée travaillant sur des nombres entiers (en réalité, en virgule fixe, mais c'est pareil).
[[File:Processeur de pixel shader de la Geforce 6800.png|centre|vignette|upright=2.0|Processeur de pixel shader de la Geforce 6800]]
==L'abandon des architectures VLIW==
Un avantage des processeurs VLIW est qu'ils ont pour particularité d'avoir un hardware très simple, avec peu de circuits de contrôle. Le compilateur se charge de vérifier que des opérations indépendantes sont regroupées dans une instruction. Alors qu'avec un processeur SIMD, il y a des circuits de détection des dépendances entre instructions bien plus complexes, avec un ''scoreboard'' et autres joyeusetés. L'absence de ces circuits fait que les processeurs VLIW étaient utilisés sur les premières cartes graphiques : autant utiliser les transistors pour placer le plus de circuits de calcul possible au lieu d'en dépenser dans des circuits de contrôle. Mais avec l'évolution de la technologie, il est devenu plus rentable d'ajouter de tels circuits pour gagner en performance.
Les processeurs VLIW sont beaucoup plus flexibles que les processeurs SIMD, car ils autorisent chaque unité de calcul à faire un calcul différent des autres, là où le SIMD force toutes les unités de calcul à faire le même calcul. Mais le problème est que cette flexibilité est peu utilisée. En effet, le compilateur doit analyser les shaders pour vérifier si des instructions peuvent être regroupées dans un ''bundle''. Le shader décrit une suite d'instructions machines, que le driver de la carte graphique analyse pour vérifier s'il peut faire des regroupements. Et disons-le clairement : les compilateurs de shaders sont assez mauvais pour ça. Ce qui fait que la flexibilité des processeurs VLIW est peu utile en pratique.
Les architectures VLIW étaient utilisés sur les premiers processeurs de shaders, et ont été abandonnées par la suite. Si de telles architectures semblent intéressantes sur le papier, cela complexifie fortement la traduction à la volée des shaders en instructions machine. Raison pour laquelle cette technique a été abandonnée.
{{NavChapitre | book=Les cartes graphiques
| prev=La microarchitecture des processeurs de shaders
| prevText=La microarchitecture des processeurs de shaders
| next=Les caches d'un processeur de shader
| netxText=Les caches d'un processeur de shader
}}{{autocat}}
tv7m8lrl4odh578tzv8wiik8gwlk1jx